Skip to main content

slt/
style.rs

1//! Visual styling primitives.
2//!
3//! Colors, themes, borders, padding, margin, constraints, alignment, and
4//! text modifiers. Every widget inherits these through [`Theme`] automatically.
5
6mod color;
7mod theme;
8pub use color::{Color, ColorDepth};
9pub use theme::{Spacing, Theme, ThemeBuilder, ThemeColor};
10
11/// Terminal size breakpoint for responsive layouts.
12///
13/// Based on the current terminal width. Use [`crate::Context::breakpoint`] to
14/// get the active breakpoint.
15#[non_exhaustive]
16#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
17pub enum Breakpoint {
18    /// Width < 40 columns (phone-sized)
19    Xs,
20    /// Width 40-79 columns (small terminal)
21    Sm,
22    /// Width 80-119 columns (standard terminal)
23    Md,
24    /// Width 120-159 columns (wide terminal)
25    Lg,
26    /// Width >= 160 columns (ultra-wide)
27    Xl,
28}
29
30/// Border style for containers.
31///
32/// Pass to `Context::bordered()` to draw a box around a container.
33/// Each variant uses a different set of Unicode box-drawing characters.
34#[non_exhaustive]
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
36#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
37pub enum Border {
38    /// Single-line box: `┌─┐│└─┘`
39    Single,
40    /// Double-line box: `╔═╗║╚═╝`
41    Double,
42    /// Rounded corners: `╭─╮│╰─╯`
43    Rounded,
44    /// Thick single-line box: `┏━┓┃┗━┛`
45    Thick,
46    /// Dashed border using light dash characters: ┄╌┄╌
47    Dashed,
48    /// Heavy dashed border: ┅╍┅╍
49    DashedThick,
50}
51
52/// Character set for a specific border style.
53///
54/// Returned by [`Border::chars`]. Contains the six box-drawing characters
55/// needed to render a complete border: four corners and two line segments.
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
57pub struct BorderChars {
58    /// Top-left corner character.
59    pub tl: char,
60    /// Top-right corner character.
61    pub tr: char,
62    /// Bottom-left corner character.
63    pub bl: char,
64    /// Bottom-right corner character.
65    pub br: char,
66    /// Horizontal line character.
67    pub h: char,
68    /// Vertical line character.
69    pub v: char,
70}
71
72/// Controls which sides of a border are visible.
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
74#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
75pub struct BorderSides {
76    /// Top border visible.
77    pub top: bool,
78    /// Right border visible.
79    pub right: bool,
80    /// Bottom border visible.
81    pub bottom: bool,
82    /// Left border visible.
83    pub left: bool,
84}
85
86impl BorderSides {
87    /// All four sides visible (default).
88    pub const fn all() -> Self {
89        Self {
90            top: true,
91            right: true,
92            bottom: true,
93            left: true,
94        }
95    }
96
97    /// No sides visible.
98    pub const fn none() -> Self {
99        Self {
100            top: false,
101            right: false,
102            bottom: false,
103            left: false,
104        }
105    }
106
107    /// Top and bottom sides only.
108    pub const fn horizontal() -> Self {
109        Self {
110            top: true,
111            right: false,
112            bottom: true,
113            left: false,
114        }
115    }
116
117    /// Left and right sides only.
118    pub const fn vertical() -> Self {
119        Self {
120            top: false,
121            right: true,
122            bottom: false,
123            left: true,
124        }
125    }
126
127    /// Returns true if top or bottom is visible.
128    pub fn has_horizontal(&self) -> bool {
129        self.top || self.bottom
130    }
131
132    /// Returns true if left or right is visible.
133    pub fn has_vertical(&self) -> bool {
134        self.left || self.right
135    }
136}
137
138impl Default for BorderSides {
139    fn default() -> Self {
140        Self::all()
141    }
142}
143
144impl Border {
145    /// Return the [`BorderChars`] for this border style.
146    pub const fn chars(self) -> BorderChars {
147        match self {
148            Self::Single => BorderChars {
149                tl: '┌',
150                tr: '┐',
151                bl: '└',
152                br: '┘',
153                h: '─',
154                v: '│',
155            },
156            Self::Double => BorderChars {
157                tl: '╔',
158                tr: '╗',
159                bl: '╚',
160                br: '╝',
161                h: '═',
162                v: '║',
163            },
164            Self::Rounded => BorderChars {
165                tl: '╭',
166                tr: '╮',
167                bl: '╰',
168                br: '╯',
169                h: '─',
170                v: '│',
171            },
172            Self::Thick => BorderChars {
173                tl: '┏',
174                tr: '┓',
175                bl: '┗',
176                br: '┛',
177                h: '━',
178                v: '┃',
179            },
180            Self::Dashed => BorderChars {
181                tl: '┌',
182                tr: '┐',
183                bl: '└',
184                br: '┘',
185                h: '┄',
186                v: '┆',
187            },
188            Self::DashedThick => BorderChars {
189                tl: '┏',
190                tr: '┓',
191                bl: '┗',
192                br: '┛',
193                h: '┅',
194                v: '┇',
195            },
196        }
197    }
198}
199
200/// Padding inside a container border.
201///
202/// Shrinks the content area inward from each edge. All values are in terminal
203/// columns/rows.
204#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
205#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
206pub struct Padding {
207    /// Padding on the top edge.
208    pub top: u32,
209    /// Padding on the right edge.
210    pub right: u32,
211    /// Padding on the bottom edge.
212    pub bottom: u32,
213    /// Padding on the left edge.
214    pub left: u32,
215}
216
217impl Padding {
218    /// Create uniform padding on all four sides.
219    pub const fn all(v: u32) -> Self {
220        Self::new(v, v, v, v)
221    }
222
223    /// Create padding with `x` on left/right and `y` on top/bottom.
224    pub const fn xy(x: u32, y: u32) -> Self {
225        Self::new(y, x, y, x)
226    }
227
228    /// Create padding with explicit values for each side.
229    pub const fn new(top: u32, right: u32, bottom: u32, left: u32) -> Self {
230        Self {
231            top,
232            right,
233            bottom,
234            left,
235        }
236    }
237
238    /// Total horizontal padding (`left + right`).
239    pub const fn horizontal(self) -> u32 {
240        self.left + self.right
241    }
242
243    /// Total vertical padding (`top + bottom`).
244    pub const fn vertical(self) -> u32 {
245        self.top + self.bottom
246    }
247}
248
249/// Margin outside a container.
250///
251/// Adds space around the outside of a container's border. All values are in
252/// terminal columns/rows.
253#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
254#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
255pub struct Margin {
256    /// Margin on the top edge.
257    pub top: u32,
258    /// Margin on the right edge.
259    pub right: u32,
260    /// Margin on the bottom edge.
261    pub bottom: u32,
262    /// Margin on the left edge.
263    pub left: u32,
264}
265
266impl Margin {
267    /// Create uniform margin on all four sides.
268    pub const fn all(v: u32) -> Self {
269        Self::new(v, v, v, v)
270    }
271
272    /// Create margin with `x` on left/right and `y` on top/bottom.
273    pub const fn xy(x: u32, y: u32) -> Self {
274        Self::new(y, x, y, x)
275    }
276
277    /// Create margin with explicit values for each side.
278    pub const fn new(top: u32, right: u32, bottom: u32, left: u32) -> Self {
279        Self {
280            top,
281            right,
282            bottom,
283            left,
284        }
285    }
286
287    /// Total horizontal margin (`left + right`).
288    pub const fn horizontal(self) -> u32 {
289        self.left + self.right
290    }
291
292    /// Total vertical margin (`top + bottom`).
293    pub const fn vertical(self) -> u32 {
294        self.top + self.bottom
295    }
296}
297
298/// 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    /// Theme-aware background color. Takes precedence over [`Self::bg`] when set.
628    pub theme_bg: Option<ThemeColor>,
629    /// Theme-aware text color. Takes precedence over [`Self::text_color`] when set.
630    pub theme_text_color: Option<ThemeColor>,
631    /// Theme-aware border foreground color. Takes precedence over
632    /// [`Self::border_style`]'s foreground when set.
633    pub theme_border_fg: Option<ThemeColor>,
634    /// Base style to inherit from. Fields in the base are applied first,
635    /// then overridden by any `Some` fields in this style.
636    ///
637    /// Use [`ContainerStyle::extending`] to create a style that inherits.
638    pub extends: Option<&'static ContainerStyle>,
639}
640
641impl ContainerStyle {
642    /// Create an empty container style with no overrides.
643    pub const fn new() -> Self {
644        Self {
645            border: None,
646            border_sides: None,
647            border_style: None,
648            bg: None,
649            text_color: None,
650            dark_bg: None,
651            dark_border_style: None,
652            padding: None,
653            margin: None,
654            gap: None,
655            row_gap: None,
656            col_gap: None,
657            grow: None,
658            align: None,
659            align_self: None,
660            justify: None,
661            w: None,
662            h: None,
663            min_w: None,
664            max_w: None,
665            min_h: None,
666            max_h: None,
667            w_pct: None,
668            h_pct: None,
669            theme_bg: None,
670            theme_text_color: None,
671            theme_border_fg: None,
672            extends: None,
673        }
674    }
675
676    /// Create a style that inherits all fields from a base style.
677    ///
678    /// Only the fields you set on the returned style will override the base.
679    /// The base must be a `&'static ContainerStyle` (typically a `const`).
680    ///
681    /// # Example
682    ///
683    /// ```ignore
684    /// use slt::{ContainerStyle, Border, ThemeColor};
685    ///
686    /// const BUTTON: ContainerStyle = ContainerStyle::new()
687    ///     .border(Border::Rounded)
688    ///     .p(1);
689    ///
690    /// const BUTTON_DANGER: ContainerStyle = ContainerStyle::extending(&BUTTON)
691    ///     .theme_bg(ThemeColor::Error);
692    /// ```
693    pub const fn extending(base: &'static ContainerStyle) -> Self {
694        let mut s = Self::new();
695        s.extends = Some(base);
696        s
697    }
698
699    /// Set the border style.
700    pub const fn border(mut self, border: Border) -> Self {
701        self.border = Some(border);
702        self
703    }
704
705    /// Set which border sides to render.
706    pub const fn border_sides(mut self, sides: BorderSides) -> Self {
707        self.border_sides = Some(sides);
708        self
709    }
710
711    /// Set the background color.
712    pub const fn bg(mut self, color: Color) -> Self {
713        self.bg = Some(color);
714        self
715    }
716
717    /// Set default text color inherited by child text widgets.
718    pub const fn text_color(mut self, color: Color) -> Self {
719        self.text_color = Some(color);
720        self
721    }
722
723    /// Set the dark-mode background color.
724    pub const fn dark_bg(mut self, color: Color) -> Self {
725        self.dark_bg = Some(color);
726        self
727    }
728
729    /// Set uniform padding on all sides.
730    pub const fn p(mut self, value: u32) -> Self {
731        self.padding = Some(Padding {
732            top: value,
733            bottom: value,
734            left: value,
735            right: value,
736        });
737        self
738    }
739
740    /// Set horizontal padding.
741    pub const fn px(mut self, value: u32) -> Self {
742        let p = match self.padding {
743            Some(p) => Padding {
744                left: value,
745                right: value,
746                ..p
747            },
748            None => Padding {
749                top: 0,
750                bottom: 0,
751                left: value,
752                right: value,
753            },
754        };
755        self.padding = Some(p);
756        self
757    }
758
759    /// Set vertical padding.
760    pub const fn py(mut self, value: u32) -> Self {
761        let p = match self.padding {
762            Some(p) => Padding {
763                top: value,
764                bottom: value,
765                ..p
766            },
767            None => Padding {
768                top: value,
769                bottom: value,
770                left: 0,
771                right: 0,
772            },
773        };
774        self.padding = Some(p);
775        self
776    }
777
778    /// Set uniform margin on all sides.
779    pub const fn m(mut self, value: u32) -> Self {
780        self.margin = Some(Margin {
781            top: value,
782            bottom: value,
783            left: value,
784            right: value,
785        });
786        self
787    }
788
789    /// Set the gap between children.
790    pub const fn gap(mut self, value: u32) -> Self {
791        self.gap = Some(value);
792        self
793    }
794
795    /// Set row gap for column layouts.
796    pub const fn row_gap(mut self, value: u32) -> Self {
797        self.row_gap = Some(value);
798        self
799    }
800
801    /// Set column gap for row layouts.
802    pub const fn col_gap(mut self, value: u32) -> Self {
803        self.col_gap = Some(value);
804        self
805    }
806
807    /// Set the flex-grow factor.
808    pub const fn grow(mut self, value: u16) -> Self {
809        self.grow = Some(value);
810        self
811    }
812
813    /// Set fixed width.
814    pub const fn w(mut self, value: u32) -> Self {
815        self.w = Some(value);
816        self
817    }
818
819    /// Set fixed height.
820    pub const fn h(mut self, value: u32) -> Self {
821        self.h = Some(value);
822        self
823    }
824
825    /// Set minimum width.
826    pub const fn min_w(mut self, value: u32) -> Self {
827        self.min_w = Some(value);
828        self
829    }
830
831    /// Set maximum width.
832    pub const fn max_w(mut self, value: u32) -> Self {
833        self.max_w = Some(value);
834        self
835    }
836
837    /// Set cross-axis alignment.
838    pub const fn align(mut self, value: Align) -> Self {
839        self.align = Some(value);
840        self
841    }
842
843    /// Set per-child cross-axis alignment override.
844    pub const fn align_self(mut self, value: Align) -> Self {
845        self.align_self = Some(value);
846        self
847    }
848
849    /// Set main-axis justification.
850    pub const fn justify(mut self, value: Justify) -> Self {
851        self.justify = Some(value);
852        self
853    }
854
855    /// Set minimum height.
856    pub const fn min_h(mut self, value: u32) -> Self {
857        self.min_h = Some(value);
858        self
859    }
860
861    /// Set maximum height.
862    pub const fn max_h(mut self, value: u32) -> Self {
863        self.max_h = Some(value);
864        self
865    }
866
867    /// Set width as percentage of parent (1-100).
868    pub const fn w_pct(mut self, value: u8) -> Self {
869        self.w_pct = Some(value);
870        self
871    }
872
873    /// Set height as percentage of parent (1-100).
874    pub const fn h_pct(mut self, value: u8) -> Self {
875        self.h_pct = Some(value);
876        self
877    }
878
879    /// Set a theme-aware background color that resolves at apply time.
880    ///
881    /// Takes precedence over [`Self::bg`] when set. The color is resolved
882    /// against the active theme when [`crate::ContainerBuilder::apply`] is called.
883    pub const fn theme_bg(mut self, color: ThemeColor) -> Self {
884        self.theme_bg = Some(color);
885        self
886    }
887
888    /// Set a theme-aware text color that resolves at apply time.
889    ///
890    /// Takes precedence over [`Self::text_color`] when set.
891    pub const fn theme_text_color(mut self, color: ThemeColor) -> Self {
892        self.theme_text_color = Some(color);
893        self
894    }
895
896    /// Set a theme-aware border foreground color that resolves at apply time.
897    ///
898    /// Takes precedence over [`Self::border_style`]'s foreground when set.
899    pub const fn theme_border_fg(mut self, color: ThemeColor) -> Self {
900        self.theme_border_fg = Some(color);
901        self
902    }
903}
904
905#[derive(Debug, Clone, Copy, Default)]
906#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
907/// Per-widget color overrides that fall back to the active theme.
908///
909/// Literal [`Color`] fields and [`ThemeColor`] fields can be set independently.
910/// Resolution order: `theme_*` field > literal field > theme default.
911pub struct WidgetColors {
912    /// Foreground color override.
913    pub fg: Option<Color>,
914    /// Background color override.
915    pub bg: Option<Color>,
916    /// Border color override.
917    pub border: Option<Color>,
918    /// Accent color override.
919    pub accent: Option<Color>,
920    /// Theme-aware foreground (takes precedence over [`Self::fg`]).
921    pub theme_fg: Option<ThemeColor>,
922    /// Theme-aware background (takes precedence over [`Self::bg`]).
923    pub theme_bg: Option<ThemeColor>,
924    /// Theme-aware border (takes precedence over [`Self::border`]).
925    pub theme_border: Option<ThemeColor>,
926    /// Theme-aware accent (takes precedence over [`Self::accent`]).
927    pub theme_accent: Option<ThemeColor>,
928}
929
930impl WidgetColors {
931    /// Create a new WidgetColors with all fields set to None (theme defaults).
932    pub const fn new() -> Self {
933        Self {
934            fg: None,
935            bg: None,
936            border: None,
937            accent: None,
938            theme_fg: None,
939            theme_bg: None,
940            theme_border: None,
941            theme_accent: None,
942        }
943    }
944
945    /// Set the foreground color override.
946    pub const fn fg(mut self, color: Color) -> Self {
947        self.fg = Some(color);
948        self
949    }
950
951    /// Set the background color override.
952    pub const fn bg(mut self, color: Color) -> Self {
953        self.bg = Some(color);
954        self
955    }
956
957    /// Set the border color override.
958    pub const fn border(mut self, color: Color) -> Self {
959        self.border = Some(color);
960        self
961    }
962
963    /// Set the accent color override.
964    pub const fn accent(mut self, color: Color) -> Self {
965        self.accent = Some(color);
966        self
967    }
968
969    /// Set a theme-aware foreground color.
970    pub const fn theme_fg(mut self, color: ThemeColor) -> Self {
971        self.theme_fg = Some(color);
972        self
973    }
974
975    /// Set a theme-aware background color.
976    pub const fn theme_bg(mut self, color: ThemeColor) -> Self {
977        self.theme_bg = Some(color);
978        self
979    }
980
981    /// Set a theme-aware border color.
982    pub const fn theme_border(mut self, color: ThemeColor) -> Self {
983        self.theme_border = Some(color);
984        self
985    }
986
987    /// Set a theme-aware accent color.
988    pub const fn theme_accent(mut self, color: ThemeColor) -> Self {
989        self.theme_accent = Some(color);
990        self
991    }
992
993    /// Resolve the foreground color, preferring theme color, then literal, then fallback.
994    pub fn resolve_fg(&self, theme: &Theme, fallback: Color) -> Color {
995        self.theme_fg
996            .map(|tc| theme.resolve(tc))
997            .or(self.fg)
998            .unwrap_or(fallback)
999    }
1000
1001    /// Resolve the background color, preferring theme color, then literal, then fallback.
1002    pub fn resolve_bg(&self, theme: &Theme, fallback: Color) -> Color {
1003        self.theme_bg
1004            .map(|tc| theme.resolve(tc))
1005            .or(self.bg)
1006            .unwrap_or(fallback)
1007    }
1008
1009    /// Resolve the border color, preferring theme color, then literal, then fallback.
1010    pub fn resolve_border(&self, theme: &Theme, fallback: Color) -> Color {
1011        self.theme_border
1012            .map(|tc| theme.resolve(tc))
1013            .or(self.border)
1014            .unwrap_or(fallback)
1015    }
1016
1017    /// Resolve the accent color, preferring theme color, then literal, then fallback.
1018    pub fn resolve_accent(&self, theme: &Theme, fallback: Color) -> Color {
1019        self.theme_accent
1020            .map(|tc| theme.resolve(tc))
1021            .or(self.accent)
1022            .unwrap_or(fallback)
1023    }
1024}
1025
1026/// Default widget colors applied to all instances of a widget type.
1027///
1028/// Set via [`crate::RunConfig::widget_theme`]. Each widget type reads its
1029/// defaults from this struct, then falls back to the active [`Theme`].
1030/// Per-callsite `_colored()` overrides still take precedence.
1031///
1032/// # Example
1033///
1034/// ```
1035/// use slt::{WidgetTheme, WidgetColors, Color};
1036///
1037/// let wt = WidgetTheme::new()
1038///     .button(WidgetColors::new().fg(Color::Cyan));
1039/// ```
1040#[derive(Debug, Clone, Copy, Default)]
1041pub struct WidgetTheme {
1042    /// Default colors for buttons.
1043    pub button: WidgetColors,
1044    /// Default colors for tables.
1045    pub table: WidgetColors,
1046    /// Default colors for lists.
1047    pub list: WidgetColors,
1048    /// Default colors for tabs.
1049    pub tabs: WidgetColors,
1050    /// Default colors for select dropdowns.
1051    pub select: WidgetColors,
1052    /// Default colors for radio groups.
1053    pub radio: WidgetColors,
1054    /// Default colors for checkboxes.
1055    pub checkbox: WidgetColors,
1056    /// Default colors for toggles.
1057    pub toggle: WidgetColors,
1058    /// Default colors for text inputs.
1059    pub text_input: WidgetColors,
1060}
1061
1062impl WidgetTheme {
1063    /// Create a WidgetTheme with all defaults (no overrides).
1064    pub const fn new() -> Self {
1065        Self {
1066            button: WidgetColors::new(),
1067            table: WidgetColors::new(),
1068            list: WidgetColors::new(),
1069            tabs: WidgetColors::new(),
1070            select: WidgetColors::new(),
1071            radio: WidgetColors::new(),
1072            checkbox: WidgetColors::new(),
1073            toggle: WidgetColors::new(),
1074            text_input: WidgetColors::new(),
1075        }
1076    }
1077
1078    /// Set default button colors.
1079    pub const fn button(mut self, colors: WidgetColors) -> Self {
1080        self.button = colors;
1081        self
1082    }
1083
1084    /// Set default table colors.
1085    pub const fn table(mut self, colors: WidgetColors) -> Self {
1086        self.table = colors;
1087        self
1088    }
1089
1090    /// Set default list colors.
1091    pub const fn list(mut self, colors: WidgetColors) -> Self {
1092        self.list = colors;
1093        self
1094    }
1095
1096    /// Set default tabs colors.
1097    pub const fn tabs(mut self, colors: WidgetColors) -> Self {
1098        self.tabs = colors;
1099        self
1100    }
1101
1102    /// Set default select colors.
1103    pub const fn select(mut self, colors: WidgetColors) -> Self {
1104        self.select = colors;
1105        self
1106    }
1107
1108    /// Set default radio colors.
1109    pub const fn radio(mut self, colors: WidgetColors) -> Self {
1110        self.radio = colors;
1111        self
1112    }
1113
1114    /// Set default checkbox colors.
1115    pub const fn checkbox(mut self, colors: WidgetColors) -> Self {
1116        self.checkbox = colors;
1117        self
1118    }
1119
1120    /// Set default toggle colors.
1121    pub const fn toggle(mut self, colors: WidgetColors) -> Self {
1122        self.toggle = colors;
1123        self
1124    }
1125
1126    /// Set default text input colors.
1127    pub const fn text_input(mut self, colors: WidgetColors) -> Self {
1128        self.text_input = colors;
1129        self
1130    }
1131}
1132
1133#[cfg(test)]
1134mod tests {
1135    use super::*;
1136
1137    #[test]
1138    fn style_new_is_default() {
1139        let style = Style::new();
1140        assert_eq!(style.fg, None);
1141        assert_eq!(style.bg, None);
1142        assert_eq!(style.modifiers, Modifiers::NONE);
1143        assert_eq!(style, Style::default());
1144    }
1145
1146    #[test]
1147    fn style_bold_and_fg_set_expected_fields() {
1148        let style = Style::new().bold().fg(Color::Red);
1149        assert_eq!(style.fg, Some(Color::Red));
1150        assert_eq!(style.bg, None);
1151        assert!(style.modifiers.contains(Modifiers::BOLD));
1152    }
1153
1154    #[test]
1155    fn style_multiple_modifiers_accumulate() {
1156        let style = Style::new().italic().underline().dim();
1157        assert!(style.modifiers.contains(Modifiers::ITALIC));
1158        assert!(style.modifiers.contains(Modifiers::UNDERLINE));
1159        assert!(style.modifiers.contains(Modifiers::DIM));
1160    }
1161
1162    #[test]
1163    fn style_repeated_fg_overrides_previous_color() {
1164        let style = Style::new().fg(Color::Blue).fg(Color::Green);
1165        assert_eq!(style.fg, Some(Color::Green));
1166    }
1167
1168    #[test]
1169    fn style_repeated_bg_overrides_previous_color() {
1170        let style = Style::new().bg(Color::Blue).bg(Color::Green);
1171        assert_eq!(style.bg, Some(Color::Green));
1172    }
1173
1174    #[test]
1175    fn style_override_preserves_existing_modifiers() {
1176        let style = Style::new().bold().fg(Color::Red).fg(Color::Yellow);
1177        assert_eq!(style.fg, Some(Color::Yellow));
1178        assert!(style.modifiers.contains(Modifiers::BOLD));
1179    }
1180
1181    #[test]
1182    fn padding_all_sets_all_sides() {
1183        let p = Padding::all(3);
1184        assert_eq!(p.top, 3);
1185        assert_eq!(p.right, 3);
1186        assert_eq!(p.bottom, 3);
1187        assert_eq!(p.left, 3);
1188    }
1189
1190    #[test]
1191    fn padding_xy_sets_axis_values() {
1192        let p = Padding::xy(4, 2);
1193        assert_eq!(p.top, 2);
1194        assert_eq!(p.bottom, 2);
1195        assert_eq!(p.left, 4);
1196        assert_eq!(p.right, 4);
1197    }
1198
1199    #[test]
1200    fn padding_new_and_totals_are_correct() {
1201        let p = Padding::new(1, 2, 3, 4);
1202        assert_eq!(p.top, 1);
1203        assert_eq!(p.right, 2);
1204        assert_eq!(p.bottom, 3);
1205        assert_eq!(p.left, 4);
1206        assert_eq!(p.horizontal(), 6);
1207        assert_eq!(p.vertical(), 4);
1208    }
1209
1210    #[test]
1211    fn margin_all_and_xy_are_correct() {
1212        let all = Margin::all(5);
1213        assert_eq!(all, Margin::new(5, 5, 5, 5));
1214
1215        let xy = Margin::xy(7, 1);
1216        assert_eq!(xy.top, 1);
1217        assert_eq!(xy.bottom, 1);
1218        assert_eq!(xy.left, 7);
1219        assert_eq!(xy.right, 7);
1220    }
1221
1222    #[test]
1223    fn margin_new_and_totals_are_correct() {
1224        let m = Margin::new(2, 4, 6, 8);
1225        assert_eq!(m.horizontal(), 12);
1226        assert_eq!(m.vertical(), 8);
1227    }
1228
1229    #[test]
1230    fn constraints_min_max_builder_sets_values() {
1231        let c = Constraints::default()
1232            .min_w(10)
1233            .max_w(40)
1234            .min_h(5)
1235            .max_h(20);
1236        assert_eq!(c.min_width, Some(10));
1237        assert_eq!(c.max_width, Some(40));
1238        assert_eq!(c.min_height, Some(5));
1239        assert_eq!(c.max_height, Some(20));
1240    }
1241
1242    #[test]
1243    fn constraints_percentage_builder_sets_values() {
1244        let c = Constraints::default().w_pct(50).h_pct(80);
1245        assert_eq!(c.width_pct, Some(50));
1246        assert_eq!(c.height_pct, Some(80));
1247    }
1248
1249    #[test]
1250    fn border_sides_all_has_both_axes() {
1251        let sides = BorderSides::all();
1252        assert!(sides.top && sides.right && sides.bottom && sides.left);
1253        assert!(sides.has_horizontal());
1254        assert!(sides.has_vertical());
1255    }
1256
1257    #[test]
1258    fn border_sides_none_has_no_axes() {
1259        let sides = BorderSides::none();
1260        assert!(!sides.top && !sides.right && !sides.bottom && !sides.left);
1261        assert!(!sides.has_horizontal());
1262        assert!(!sides.has_vertical());
1263    }
1264
1265    #[test]
1266    fn border_sides_horizontal_only() {
1267        let sides = BorderSides::horizontal();
1268        assert!(sides.top);
1269        assert!(sides.bottom);
1270        assert!(!sides.left);
1271        assert!(!sides.right);
1272        assert!(sides.has_horizontal());
1273        assert!(!sides.has_vertical());
1274    }
1275
1276    #[test]
1277    fn border_sides_vertical_only() {
1278        let sides = BorderSides::vertical();
1279        assert!(!sides.top);
1280        assert!(!sides.bottom);
1281        assert!(sides.left);
1282        assert!(sides.right);
1283        assert!(!sides.has_horizontal());
1284        assert!(sides.has_vertical());
1285    }
1286
1287    #[test]
1288    fn container_style_new_is_empty() {
1289        let s = ContainerStyle::new();
1290        assert_eq!(s.border, None);
1291        assert_eq!(s.bg, None);
1292        assert_eq!(s.padding, None);
1293        assert_eq!(s.margin, None);
1294        assert_eq!(s.gap, None);
1295        assert_eq!(s.align, None);
1296        assert_eq!(s.justify, None);
1297    }
1298
1299    #[test]
1300    fn container_style_const_construction_and_fields() {
1301        const CARD: ContainerStyle = ContainerStyle::new()
1302            .border(Border::Rounded)
1303            .border_sides(BorderSides::horizontal())
1304            .p(2)
1305            .m(1)
1306            .gap(3)
1307            .align(Align::Center)
1308            .justify(Justify::SpaceBetween)
1309            .w(60)
1310            .h(20);
1311
1312        assert_eq!(CARD.border, Some(Border::Rounded));
1313        assert_eq!(CARD.border_sides, Some(BorderSides::horizontal()));
1314        assert_eq!(CARD.padding, Some(Padding::all(2)));
1315        assert_eq!(CARD.margin, Some(Margin::all(1)));
1316        assert_eq!(CARD.gap, Some(3));
1317        assert_eq!(CARD.align, Some(Align::Center));
1318        assert_eq!(CARD.justify, Some(Justify::SpaceBetween));
1319        assert_eq!(CARD.w, Some(60));
1320        assert_eq!(CARD.h, Some(20));
1321    }
1322
1323    #[test]
1324    fn widget_colors_new_is_empty() {
1325        let colors = WidgetColors::new();
1326        assert_eq!(colors.fg, None);
1327        assert_eq!(colors.bg, None);
1328        assert_eq!(colors.border, None);
1329        assert_eq!(colors.accent, None);
1330
1331        let defaults = WidgetColors::default();
1332        assert_eq!(defaults.fg, None);
1333        assert_eq!(defaults.bg, None);
1334        assert_eq!(defaults.border, None);
1335        assert_eq!(defaults.accent, None);
1336    }
1337
1338    #[test]
1339    fn widget_colors_builder_sets_all_fields() {
1340        let colors = WidgetColors::new()
1341            .fg(Color::White)
1342            .bg(Color::Black)
1343            .border(Color::Cyan)
1344            .accent(Color::Yellow);
1345
1346        assert_eq!(colors.fg, Some(Color::White));
1347        assert_eq!(colors.bg, Some(Color::Black));
1348        assert_eq!(colors.border, Some(Color::Cyan));
1349        assert_eq!(colors.accent, Some(Color::Yellow));
1350    }
1351
1352    #[test]
1353    fn align_default_is_start() {
1354        assert_eq!(Align::default(), Align::Start);
1355    }
1356
1357    #[test]
1358    fn justify_default_is_start() {
1359        assert_eq!(Justify::default(), Justify::Start);
1360    }
1361
1362    #[test]
1363    fn align_and_justify_variants_are_distinct() {
1364        assert_ne!(Align::Start, Align::Center);
1365        assert_ne!(Align::Center, Align::End);
1366
1367        assert_ne!(Justify::Start, Justify::Center);
1368        assert_ne!(Justify::Center, Justify::End);
1369        assert_ne!(Justify::SpaceBetween, Justify::SpaceAround);
1370        assert_ne!(Justify::SpaceAround, Justify::SpaceEvenly);
1371    }
1372}