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
6/// Terminal color.
7///
8/// Covers the standard 16 named colors, 256-color palette indices, and
9/// 24-bit RGB true color. Use [`Color::Reset`] to restore the terminal's
10/// default foreground or background.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
13pub enum Color {
14    /// Reset to the terminal's default color.
15    Reset,
16    /// Standard black (color index 0).
17    Black,
18    /// Standard red (color index 1).
19    Red,
20    /// Standard green (color index 2).
21    Green,
22    /// Standard yellow (color index 3).
23    Yellow,
24    /// Standard blue (color index 4).
25    Blue,
26    /// Standard magenta (color index 5).
27    Magenta,
28    /// Standard cyan (color index 6).
29    Cyan,
30    /// Standard white (color index 7).
31    White,
32    /// 24-bit true color.
33    Rgb(u8, u8, u8),
34    /// 256-color palette index.
35    Indexed(u8),
36}
37
38impl Color {
39    /// Resolve to `(r, g, b)` for luminance and blending operations.
40    ///
41    /// Named colors map to their typical terminal palette values.
42    /// [`Color::Reset`] maps to black; [`Color::Indexed`] maps to the xterm-256 palette.
43    fn to_rgb(self) -> (u8, u8, u8) {
44        match self {
45            Color::Rgb(r, g, b) => (r, g, b),
46            Color::Black => (0, 0, 0),
47            Color::Red => (205, 49, 49),
48            Color::Green => (13, 188, 121),
49            Color::Yellow => (229, 229, 16),
50            Color::Blue => (36, 114, 200),
51            Color::Magenta => (188, 63, 188),
52            Color::Cyan => (17, 168, 205),
53            Color::White => (229, 229, 229),
54            Color::Reset => (0, 0, 0),
55            Color::Indexed(idx) => xterm256_to_rgb(idx),
56        }
57    }
58
59    /// Compute relative luminance using ITU-R BT.709 coefficients.
60    ///
61    /// Returns a value in `[0.0, 1.0]` where 0 is darkest and 1 is brightest.
62    /// Use this to determine whether text on a given background should be
63    /// light or dark.
64    ///
65    /// # Example
66    ///
67    /// ```
68    /// use slt::Color;
69    ///
70    /// let dark = Color::Rgb(30, 30, 46);
71    /// assert!(dark.luminance() < 0.15);
72    ///
73    /// let light = Color::Rgb(205, 214, 244);
74    /// assert!(light.luminance() > 0.6);
75    /// ```
76    pub fn luminance(self) -> f32 {
77        let (r, g, b) = self.to_rgb();
78        let rf = r as f32 / 255.0;
79        let gf = g as f32 / 255.0;
80        let bf = b as f32 / 255.0;
81        0.2126 * rf + 0.7152 * gf + 0.0722 * bf
82    }
83
84    /// Return a contrasting foreground color for the given background.
85    ///
86    /// Uses the BT.709 luminance threshold (0.5) to decide between white
87    /// and black text. For theme-aware contrast, prefer using this over
88    /// hardcoding `theme.bg` as the foreground.
89    ///
90    /// # Example
91    ///
92    /// ```
93    /// use slt::Color;
94    ///
95    /// let bg = Color::Rgb(189, 147, 249); // Dracula purple
96    /// let fg = Color::contrast_fg(bg);
97    /// // Purple is mid-bright → returns black for readable text
98    /// ```
99    pub fn contrast_fg(bg: Color) -> Color {
100        if bg.luminance() > 0.5 {
101            Color::Rgb(0, 0, 0)
102        } else {
103            Color::Rgb(255, 255, 255)
104        }
105    }
106
107    /// Blend this color over another with the given alpha.
108    ///
109    /// `alpha` is in `[0.0, 1.0]` where 0.0 returns `other` unchanged and
110    /// 1.0 returns `self` unchanged. Both colors are resolved to RGB.
111    ///
112    /// # Example
113    ///
114    /// ```
115    /// use slt::Color;
116    ///
117    /// let white = Color::Rgb(255, 255, 255);
118    /// let black = Color::Rgb(0, 0, 0);
119    /// let gray = white.blend(black, 0.5);
120    /// // ≈ Rgb(128, 128, 128)
121    /// ```
122    pub fn blend(self, other: Color, alpha: f32) -> Color {
123        let alpha = alpha.clamp(0.0, 1.0);
124        let (r1, g1, b1) = self.to_rgb();
125        let (r2, g2, b2) = other.to_rgb();
126        let r = (r1 as f32 * alpha + r2 as f32 * (1.0 - alpha)) as u8;
127        let g = (g1 as f32 * alpha + g2 as f32 * (1.0 - alpha)) as u8;
128        let b = (b1 as f32 * alpha + b2 as f32 * (1.0 - alpha)) as u8;
129        Color::Rgb(r, g, b)
130    }
131
132    /// Lighten this color by the given amount (0.0–1.0).
133    ///
134    /// Blends toward white. `amount = 0.0` returns the original color;
135    /// `amount = 1.0` returns white.
136    pub fn lighten(self, amount: f32) -> Color {
137        Color::Rgb(255, 255, 255).blend(self, 1.0 - amount.clamp(0.0, 1.0))
138    }
139
140    /// Darken this color by the given amount (0.0–1.0).
141    ///
142    /// Blends toward black. `amount = 0.0` returns the original color;
143    /// `amount = 1.0` returns black.
144    pub fn darken(self, amount: f32) -> Color {
145        Color::Rgb(0, 0, 0).blend(self, 1.0 - amount.clamp(0.0, 1.0))
146    }
147
148    /// Downsample this color to fit the given color depth.
149    ///
150    /// - `TrueColor`: returns self unchanged.
151    /// - `EightBit`: converts `Rgb` to the nearest `Indexed` color.
152    /// - `Basic`: converts `Rgb` and `Indexed` to the nearest named color.
153    ///
154    /// Named colors (`Red`, `Green`, etc.) and `Reset` pass through all depths.
155    pub fn downsampled(self, depth: ColorDepth) -> Color {
156        match depth {
157            ColorDepth::TrueColor => self,
158            ColorDepth::EightBit => match self {
159                Color::Rgb(r, g, b) => Color::Indexed(rgb_to_ansi256(r, g, b)),
160                other => other,
161            },
162            ColorDepth::Basic => match self {
163                Color::Rgb(r, g, b) => rgb_to_ansi16(r, g, b),
164                Color::Indexed(i) => {
165                    let (r, g, b) = xterm256_to_rgb(i);
166                    rgb_to_ansi16(r, g, b)
167                }
168                other => other,
169            },
170        }
171    }
172}
173
174/// Terminal color depth capability.
175///
176/// Determines the maximum number of colors a terminal can display.
177/// Use [`ColorDepth::detect`] for automatic detection via environment
178/// variables, or specify explicitly in [`RunConfig`].
179#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
180#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
181pub enum ColorDepth {
182    /// 24-bit true color (16 million colors).
183    TrueColor,
184    /// 256-color palette (xterm-256color).
185    EightBit,
186    /// 16 basic ANSI colors.
187    Basic,
188}
189
190impl ColorDepth {
191    /// Detect the terminal's color depth from environment variables.
192    ///
193    /// Checks `$COLORTERM` for `truecolor`/`24bit`, then `$TERM` for
194    /// `256color`. Falls back to `Basic` (16 colors) if neither is set.
195    pub fn detect() -> Self {
196        if let Ok(ct) = std::env::var("COLORTERM") {
197            let ct = ct.to_lowercase();
198            if ct == "truecolor" || ct == "24bit" {
199                return Self::TrueColor;
200            }
201        }
202        if let Ok(term) = std::env::var("TERM") {
203            if term.contains("256color") {
204                return Self::EightBit;
205            }
206        }
207        Self::Basic
208    }
209}
210
211fn rgb_to_ansi256(r: u8, g: u8, b: u8) -> u8 {
212    if r == g && g == b {
213        if r < 8 {
214            return 16;
215        }
216        if r > 248 {
217            return 231;
218        }
219        return 232 + (((r as u16 - 8) * 24 / 240) as u8);
220    }
221
222    let ri = if r < 48 {
223        0
224    } else {
225        ((r as u16 - 35) / 40) as u8
226    };
227    let gi = if g < 48 {
228        0
229    } else {
230        ((g as u16 - 35) / 40) as u8
231    };
232    let bi = if b < 48 {
233        0
234    } else {
235        ((b as u16 - 35) / 40) as u8
236    };
237    16 + 36 * ri.min(5) + 6 * gi.min(5) + bi.min(5)
238}
239
240fn rgb_to_ansi16(r: u8, g: u8, b: u8) -> Color {
241    let lum =
242        0.2126 * (r as f32 / 255.0) + 0.7152 * (g as f32 / 255.0) + 0.0722 * (b as f32 / 255.0);
243
244    let max = r.max(g).max(b);
245    let min = r.min(g).min(b);
246    let saturation = if max == 0 {
247        0.0
248    } else {
249        (max - min) as f32 / max as f32
250    };
251
252    if saturation < 0.2 {
253        return if lum < 0.15 {
254            Color::Black
255        } else {
256            Color::White
257        };
258    }
259
260    let rf = r as f32;
261    let gf = g as f32;
262    let bf = b as f32;
263
264    if rf >= gf && rf >= bf {
265        if gf > bf * 1.5 {
266            Color::Yellow
267        } else if bf > gf * 1.5 {
268            Color::Magenta
269        } else {
270            Color::Red
271        }
272    } else if gf >= rf && gf >= bf {
273        if bf > rf * 1.5 {
274            Color::Cyan
275        } else {
276            Color::Green
277        }
278    } else if rf > gf * 1.5 {
279        Color::Magenta
280    } else if gf > rf * 1.5 {
281        Color::Cyan
282    } else {
283        Color::Blue
284    }
285}
286
287fn xterm256_to_rgb(idx: u8) -> (u8, u8, u8) {
288    match idx {
289        0 => (0, 0, 0),
290        1 => (128, 0, 0),
291        2 => (0, 128, 0),
292        3 => (128, 128, 0),
293        4 => (0, 0, 128),
294        5 => (128, 0, 128),
295        6 => (0, 128, 128),
296        7 => (192, 192, 192),
297        8 => (128, 128, 128),
298        9 => (255, 0, 0),
299        10 => (0, 255, 0),
300        11 => (255, 255, 0),
301        12 => (0, 0, 255),
302        13 => (255, 0, 255),
303        14 => (0, 255, 255),
304        15 => (255, 255, 255),
305        16..=231 => {
306            let n = idx - 16;
307            let b_idx = n % 6;
308            let g_idx = (n / 6) % 6;
309            let r_idx = n / 36;
310            let to_val = |i: u8| if i == 0 { 0u8 } else { 55 + 40 * i };
311            (to_val(r_idx), to_val(g_idx), to_val(b_idx))
312        }
313        232..=255 => {
314            let v = 8 + 10 * (idx - 232);
315            (v, v, v)
316        }
317    }
318}
319
320/// A color theme that flows through all widgets automatically.
321///
322/// Construct with [`Theme::dark()`] or [`Theme::light()`], or build a custom
323/// theme by filling in the fields directly. Pass the theme via [`crate::RunConfig`]
324/// and every widget will pick up the colors without any extra wiring.
325#[derive(Debug, Clone, Copy)]
326#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
327pub struct Theme {
328    /// Primary accent color, used for focused borders and highlights.
329    pub primary: Color,
330    /// Secondary accent color, used for less prominent highlights.
331    pub secondary: Color,
332    /// Accent color for decorative elements.
333    pub accent: Color,
334    /// Default foreground text color.
335    pub text: Color,
336    /// Dimmed text color for secondary labels and hints.
337    pub text_dim: Color,
338    /// Border color for unfocused containers.
339    pub border: Color,
340    /// Background color. Typically [`Color::Reset`] to inherit the terminal background.
341    pub bg: Color,
342    /// Color for success states (e.g., toast notifications).
343    pub success: Color,
344    /// Color for warning states.
345    pub warning: Color,
346    /// Color for error states.
347    pub error: Color,
348    /// Background color for selected list/table rows.
349    pub selected_bg: Color,
350    /// Foreground color for selected list/table rows.
351    pub selected_fg: Color,
352    /// Subtle surface color for card backgrounds and elevated containers.
353    pub surface: Color,
354    /// Hover/active surface color, one step brighter than `surface`.
355    ///
356    /// Used for interactive element hover states. Should be visually
357    /// distinguishable from both `surface` and `border`.
358    pub surface_hover: Color,
359    /// Secondary text color guaranteed readable on `surface` backgrounds.
360    ///
361    /// Use this instead of `text_dim` when rendering on `surface`-colored
362    /// containers. `text_dim` is tuned for the main `bg`; on `surface` it
363    /// may lack contrast.
364    pub surface_text: Color,
365}
366
367impl Theme {
368    /// Create a dark theme with cyan primary and white text.
369    pub fn dark() -> Self {
370        Self {
371            primary: Color::Cyan,
372            secondary: Color::Blue,
373            accent: Color::Magenta,
374            text: Color::White,
375            text_dim: Color::Indexed(245),
376            border: Color::Indexed(240),
377            bg: Color::Reset,
378            success: Color::Green,
379            warning: Color::Yellow,
380            error: Color::Red,
381            selected_bg: Color::Cyan,
382            selected_fg: Color::Black,
383            surface: Color::Indexed(236),
384            surface_hover: Color::Indexed(238),
385            surface_text: Color::Indexed(250),
386        }
387    }
388
389    /// Create a light theme with blue primary and black text.
390    pub fn light() -> Self {
391        Self {
392            primary: Color::Blue,
393            secondary: Color::Cyan,
394            accent: Color::Magenta,
395            text: Color::Black,
396            text_dim: Color::Indexed(240),
397            border: Color::Indexed(245),
398            bg: Color::Reset,
399            success: Color::Green,
400            warning: Color::Yellow,
401            error: Color::Red,
402            selected_bg: Color::Blue,
403            selected_fg: Color::White,
404            surface: Color::Indexed(254),
405            surface_hover: Color::Indexed(252),
406            surface_text: Color::Indexed(238),
407        }
408    }
409
410    /// Dracula theme — purple primary on dark gray.
411    pub fn dracula() -> Self {
412        Self {
413            primary: Color::Rgb(189, 147, 249),
414            secondary: Color::Rgb(139, 233, 253),
415            accent: Color::Rgb(255, 121, 198),
416            text: Color::Rgb(248, 248, 242),
417            text_dim: Color::Rgb(98, 114, 164),
418            border: Color::Rgb(68, 71, 90),
419            bg: Color::Rgb(40, 42, 54),
420            success: Color::Rgb(80, 250, 123),
421            warning: Color::Rgb(241, 250, 140),
422            error: Color::Rgb(255, 85, 85),
423            selected_bg: Color::Rgb(189, 147, 249),
424            selected_fg: Color::Rgb(40, 42, 54),
425            surface: Color::Rgb(68, 71, 90),
426            surface_hover: Color::Rgb(98, 100, 120),
427            surface_text: Color::Rgb(191, 194, 210),
428        }
429    }
430
431    /// Catppuccin Mocha theme — lavender primary on dark base.
432    pub fn catppuccin() -> Self {
433        Self {
434            primary: Color::Rgb(180, 190, 254),
435            secondary: Color::Rgb(137, 180, 250),
436            accent: Color::Rgb(245, 194, 231),
437            text: Color::Rgb(205, 214, 244),
438            text_dim: Color::Rgb(127, 132, 156),
439            border: Color::Rgb(88, 91, 112),
440            bg: Color::Rgb(30, 30, 46),
441            success: Color::Rgb(166, 227, 161),
442            warning: Color::Rgb(249, 226, 175),
443            error: Color::Rgb(243, 139, 168),
444            selected_bg: Color::Rgb(180, 190, 254),
445            selected_fg: Color::Rgb(30, 30, 46),
446            surface: Color::Rgb(49, 50, 68),
447            surface_hover: Color::Rgb(69, 71, 90),
448            surface_text: Color::Rgb(166, 173, 200),
449        }
450    }
451
452    /// Nord theme — frost blue primary on polar night.
453    pub fn nord() -> Self {
454        Self {
455            primary: Color::Rgb(136, 192, 208),
456            secondary: Color::Rgb(129, 161, 193),
457            accent: Color::Rgb(180, 142, 173),
458            text: Color::Rgb(236, 239, 244),
459            text_dim: Color::Rgb(76, 86, 106),
460            border: Color::Rgb(76, 86, 106),
461            bg: Color::Rgb(46, 52, 64),
462            success: Color::Rgb(163, 190, 140),
463            warning: Color::Rgb(235, 203, 139),
464            error: Color::Rgb(191, 97, 106),
465            selected_bg: Color::Rgb(136, 192, 208),
466            selected_fg: Color::Rgb(46, 52, 64),
467            surface: Color::Rgb(59, 66, 82),
468            surface_hover: Color::Rgb(67, 76, 94),
469            surface_text: Color::Rgb(216, 222, 233),
470        }
471    }
472
473    /// Solarized Dark theme — blue primary on dark base.
474    pub fn solarized_dark() -> Self {
475        Self {
476            primary: Color::Rgb(38, 139, 210),
477            secondary: Color::Rgb(42, 161, 152),
478            accent: Color::Rgb(211, 54, 130),
479            text: Color::Rgb(131, 148, 150),
480            text_dim: Color::Rgb(88, 110, 117),
481            border: Color::Rgb(88, 110, 117),
482            bg: Color::Rgb(0, 43, 54),
483            success: Color::Rgb(133, 153, 0),
484            warning: Color::Rgb(181, 137, 0),
485            error: Color::Rgb(220, 50, 47),
486            selected_bg: Color::Rgb(38, 139, 210),
487            selected_fg: Color::Rgb(253, 246, 227),
488            surface: Color::Rgb(7, 54, 66),
489            surface_hover: Color::Rgb(23, 72, 85),
490            surface_text: Color::Rgb(147, 161, 161),
491        }
492    }
493
494    /// Tokyo Night theme — blue primary on dark storm base.
495    pub fn tokyo_night() -> Self {
496        Self {
497            primary: Color::Rgb(122, 162, 247),
498            secondary: Color::Rgb(125, 207, 255),
499            accent: Color::Rgb(187, 154, 247),
500            text: Color::Rgb(169, 177, 214),
501            text_dim: Color::Rgb(86, 95, 137),
502            border: Color::Rgb(54, 58, 79),
503            bg: Color::Rgb(26, 27, 38),
504            success: Color::Rgb(158, 206, 106),
505            warning: Color::Rgb(224, 175, 104),
506            error: Color::Rgb(247, 118, 142),
507            selected_bg: Color::Rgb(122, 162, 247),
508            selected_fg: Color::Rgb(26, 27, 38),
509            surface: Color::Rgb(36, 40, 59),
510            surface_hover: Color::Rgb(41, 46, 66),
511            surface_text: Color::Rgb(192, 202, 245),
512        }
513    }
514}
515
516impl Default for Theme {
517    fn default() -> Self {
518        Self::dark()
519    }
520}
521
522/// Terminal size breakpoint for responsive layouts.
523///
524/// Based on the current terminal width. Use [`Context::breakpoint`] to
525/// get the active breakpoint.
526#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
527pub enum Breakpoint {
528    /// Width < 40 columns (phone-sized)
529    Xs,
530    /// Width 40-79 columns (small terminal)
531    Sm,
532    /// Width 80-119 columns (standard terminal)
533    Md,
534    /// Width 120-159 columns (wide terminal)
535    Lg,
536    /// Width >= 160 columns (ultra-wide)
537    Xl,
538}
539
540/// Border style for containers.
541///
542/// Pass to `Context::bordered()` to draw a box around a container.
543/// Each variant uses a different set of Unicode box-drawing characters.
544#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
545#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
546pub enum Border {
547    /// Single-line box: `┌─┐│└─┘`
548    Single,
549    /// Double-line box: `╔═╗║╚═╝`
550    Double,
551    /// Rounded corners: `╭─╮│╰─╯`
552    Rounded,
553    /// Thick single-line box: `┏━┓┃┗━┛`
554    Thick,
555    /// Dashed border using light dash characters: ┄╌┄╌
556    Dashed,
557    /// Heavy dashed border: ┅╍┅╍
558    DashedThick,
559}
560
561/// Character set for a specific border style.
562///
563/// Returned by [`Border::chars`]. Contains the six box-drawing characters
564/// needed to render a complete border: four corners and two line segments.
565#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
566pub struct BorderChars {
567    /// Top-left corner character.
568    pub tl: char,
569    /// Top-right corner character.
570    pub tr: char,
571    /// Bottom-left corner character.
572    pub bl: char,
573    /// Bottom-right corner character.
574    pub br: char,
575    /// Horizontal line character.
576    pub h: char,
577    /// Vertical line character.
578    pub v: char,
579}
580
581/// Controls which sides of a border are visible.
582#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
583#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
584pub struct BorderSides {
585    pub top: bool,
586    pub right: bool,
587    pub bottom: bool,
588    pub left: bool,
589}
590
591impl BorderSides {
592    pub const fn all() -> Self {
593        Self {
594            top: true,
595            right: true,
596            bottom: true,
597            left: true,
598        }
599    }
600
601    pub const fn none() -> Self {
602        Self {
603            top: false,
604            right: false,
605            bottom: false,
606            left: false,
607        }
608    }
609
610    pub const fn horizontal() -> Self {
611        Self {
612            top: true,
613            right: false,
614            bottom: true,
615            left: false,
616        }
617    }
618
619    pub const fn vertical() -> Self {
620        Self {
621            top: false,
622            right: true,
623            bottom: false,
624            left: true,
625        }
626    }
627
628    pub fn has_horizontal(&self) -> bool {
629        self.top || self.bottom
630    }
631
632    pub fn has_vertical(&self) -> bool {
633        self.left || self.right
634    }
635}
636
637impl Default for BorderSides {
638    fn default() -> Self {
639        Self::all()
640    }
641}
642
643impl Border {
644    /// Return the [`BorderChars`] for this border style.
645    pub const fn chars(self) -> BorderChars {
646        match self {
647            Self::Single => BorderChars {
648                tl: '┌',
649                tr: '┐',
650                bl: '└',
651                br: '┘',
652                h: '─',
653                v: '│',
654            },
655            Self::Double => BorderChars {
656                tl: '╔',
657                tr: '╗',
658                bl: '╚',
659                br: '╝',
660                h: '═',
661                v: '║',
662            },
663            Self::Rounded => BorderChars {
664                tl: '╭',
665                tr: '╮',
666                bl: '╰',
667                br: '╯',
668                h: '─',
669                v: '│',
670            },
671            Self::Thick => BorderChars {
672                tl: '┏',
673                tr: '┓',
674                bl: '┗',
675                br: '┛',
676                h: '━',
677                v: '┃',
678            },
679            Self::Dashed => BorderChars {
680                tl: '┌',
681                tr: '┐',
682                bl: '└',
683                br: '┘',
684                h: '┄',
685                v: '┆',
686            },
687            Self::DashedThick => BorderChars {
688                tl: '┏',
689                tr: '┓',
690                bl: '┗',
691                br: '┛',
692                h: '┅',
693                v: '┇',
694            },
695        }
696    }
697}
698
699/// Padding inside a container border.
700///
701/// Shrinks the content area inward from each edge. All values are in terminal
702/// columns/rows.
703#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
704#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
705pub struct Padding {
706    /// Padding on the top edge.
707    pub top: u32,
708    /// Padding on the right edge.
709    pub right: u32,
710    /// Padding on the bottom edge.
711    pub bottom: u32,
712    /// Padding on the left edge.
713    pub left: u32,
714}
715
716impl Padding {
717    /// Create uniform padding on all four sides.
718    pub const fn all(v: u32) -> Self {
719        Self::new(v, v, v, v)
720    }
721
722    /// Create padding with `x` on left/right and `y` on top/bottom.
723    pub const fn xy(x: u32, y: u32) -> Self {
724        Self::new(y, x, y, x)
725    }
726
727    /// Create padding with explicit values for each side.
728    pub const fn new(top: u32, right: u32, bottom: u32, left: u32) -> Self {
729        Self {
730            top,
731            right,
732            bottom,
733            left,
734        }
735    }
736
737    /// Total horizontal padding (`left + right`).
738    pub const fn horizontal(self) -> u32 {
739        self.left + self.right
740    }
741
742    /// Total vertical padding (`top + bottom`).
743    pub const fn vertical(self) -> u32 {
744        self.top + self.bottom
745    }
746}
747
748/// Margin outside a container.
749///
750/// Adds space around the outside of a container's border. All values are in
751/// terminal columns/rows.
752#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
753#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
754pub struct Margin {
755    /// Margin on the top edge.
756    pub top: u32,
757    /// Margin on the right edge.
758    pub right: u32,
759    /// Margin on the bottom edge.
760    pub bottom: u32,
761    /// Margin on the left edge.
762    pub left: u32,
763}
764
765impl Margin {
766    /// Create uniform margin on all four sides.
767    pub const fn all(v: u32) -> Self {
768        Self::new(v, v, v, v)
769    }
770
771    /// Create margin with `x` on left/right and `y` on top/bottom.
772    pub const fn xy(x: u32, y: u32) -> Self {
773        Self::new(y, x, y, x)
774    }
775
776    /// Create margin with explicit values for each side.
777    pub const fn new(top: u32, right: u32, bottom: u32, left: u32) -> Self {
778        Self {
779            top,
780            right,
781            bottom,
782            left,
783        }
784    }
785
786    /// Total horizontal margin (`left + right`).
787    pub const fn horizontal(self) -> u32 {
788        self.left + self.right
789    }
790
791    /// Total vertical margin (`top + bottom`).
792    pub const fn vertical(self) -> u32 {
793        self.top + self.bottom
794    }
795}
796
797/// Size constraints for layout computation.
798///
799/// All fields are optional. Unset constraints are unconstrained. Use the
800/// builder methods to set individual bounds in a fluent style.
801///
802/// # Example
803///
804/// ```
805/// use slt::Constraints;
806///
807/// let c = Constraints::default().min_w(10).max_w(40);
808/// ```
809#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
810#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
811#[must_use = "configure constraints using the returned value"]
812pub struct Constraints {
813    /// Minimum width in terminal columns, if any.
814    pub min_width: Option<u32>,
815    /// Maximum width in terminal columns, if any.
816    pub max_width: Option<u32>,
817    /// Minimum height in terminal rows, if any.
818    pub min_height: Option<u32>,
819    /// Maximum height in terminal rows, if any.
820    pub max_height: Option<u32>,
821    /// Width as a percentage (1-100) of the parent container.
822    pub width_pct: Option<u8>,
823    /// Height as a percentage (1-100) of the parent container.
824    pub height_pct: Option<u8>,
825}
826
827impl Constraints {
828    /// Set the minimum width constraint.
829    pub const fn min_w(mut self, min_width: u32) -> Self {
830        self.min_width = Some(min_width);
831        self
832    }
833
834    /// Set the maximum width constraint.
835    pub const fn max_w(mut self, max_width: u32) -> Self {
836        self.max_width = Some(max_width);
837        self
838    }
839
840    /// Set the minimum height constraint.
841    pub const fn min_h(mut self, min_height: u32) -> Self {
842        self.min_height = Some(min_height);
843        self
844    }
845
846    /// Set the maximum height constraint.
847    pub const fn max_h(mut self, max_height: u32) -> Self {
848        self.max_height = Some(max_height);
849        self
850    }
851
852    /// Set width as a percentage (1-100) of the parent container.
853    pub const fn w_pct(mut self, pct: u8) -> Self {
854        self.width_pct = Some(pct);
855        self
856    }
857
858    /// Set height as a percentage (1-100) of the parent container.
859    pub const fn h_pct(mut self, pct: u8) -> Self {
860        self.height_pct = Some(pct);
861        self
862    }
863}
864
865/// Cross-axis alignment within a container.
866///
867/// Controls how children are positioned along the axis perpendicular to the
868/// container's main axis. For a `row()`, this is vertical alignment; for a
869/// `col()`, this is horizontal alignment.
870#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
871#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
872pub enum Align {
873    /// Align children to the start of the cross axis (default).
874    #[default]
875    Start,
876    /// Center children on the cross axis.
877    Center,
878    /// Align children to the end of the cross axis.
879    End,
880}
881
882/// Main-axis content distribution within a container.
883///
884/// Controls how children are distributed along the main axis. For a `row()`,
885/// this is horizontal distribution; for a `col()`, this is vertical.
886///
887/// When children have `grow > 0`, they consume remaining space before justify
888/// distribution applies. Justify modes only affect the leftover space after
889/// flex-grow allocation.
890#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
891#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
892pub enum Justify {
893    /// Pack children at the start (default). Uses `gap` for spacing.
894    #[default]
895    Start,
896    /// Center children along the main axis with `gap` spacing.
897    Center,
898    /// Pack children at the end with `gap` spacing.
899    End,
900    /// First child at start, last at end, equal space between.
901    SpaceBetween,
902    /// Equal space around each child (half-size space at edges).
903    SpaceAround,
904    /// Equal space between all children and at both edges.
905    SpaceEvenly,
906}
907
908/// Text modifier bitflags stored as a `u8`.
909///
910/// Combine modifiers with `|` or [`Modifiers::insert`]. Check membership with
911/// [`Modifiers::contains`].
912#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
913#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
914#[cfg_attr(feature = "serde", serde(transparent))]
915pub struct Modifiers(pub u8);
916
917impl Modifiers {
918    /// No modifiers set.
919    pub const NONE: Self = Self(0);
920    /// Bold text.
921    pub const BOLD: Self = Self(1 << 0);
922    /// Dimmed/faint text.
923    pub const DIM: Self = Self(1 << 1);
924    /// Italic text.
925    pub const ITALIC: Self = Self(1 << 2);
926    /// Underlined text.
927    pub const UNDERLINE: Self = Self(1 << 3);
928    /// Reversed foreground/background colors.
929    pub const REVERSED: Self = Self(1 << 4);
930    /// Strikethrough text.
931    pub const STRIKETHROUGH: Self = Self(1 << 5);
932
933    /// Returns `true` if all bits in `other` are set in `self`.
934    #[inline]
935    pub fn contains(self, other: Self) -> bool {
936        (self.0 & other.0) == other.0
937    }
938
939    /// Set all bits from `other` into `self`.
940    #[inline]
941    pub fn insert(&mut self, other: Self) {
942        self.0 |= other.0;
943    }
944
945    /// Returns `true` if no modifiers are set.
946    #[inline]
947    pub fn is_empty(self) -> bool {
948        self.0 == 0
949    }
950}
951
952impl std::ops::BitOr for Modifiers {
953    type Output = Self;
954    #[inline]
955    fn bitor(self, rhs: Self) -> Self {
956        Self(self.0 | rhs.0)
957    }
958}
959
960impl std::ops::BitOrAssign for Modifiers {
961    #[inline]
962    fn bitor_assign(&mut self, rhs: Self) {
963        self.0 |= rhs.0;
964    }
965}
966
967/// Visual style for a terminal cell (foreground, background, modifiers).
968///
969/// Styles are applied to text via the builder methods on `Context` widget
970/// calls (e.g., `.bold()`, `.fg(Color::Cyan)`). All fields are optional;
971/// `None` means "inherit from the terminal default."
972///
973/// # Example
974///
975/// ```
976/// use slt::{Style, Color};
977///
978/// let style = Style::new().fg(Color::Cyan).bold();
979/// ```
980#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
981#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
982#[must_use = "build and pass the returned Style value"]
983pub struct Style {
984    /// Foreground color, or `None` to use the terminal default.
985    pub fg: Option<Color>,
986    /// Background color, or `None` to use the terminal default.
987    pub bg: Option<Color>,
988    /// Text modifiers (bold, italic, underline, etc.).
989    pub modifiers: Modifiers,
990}
991
992impl Style {
993    /// Create a new style with no color or modifiers set.
994    pub const fn new() -> Self {
995        Self {
996            fg: None,
997            bg: None,
998            modifiers: Modifiers::NONE,
999        }
1000    }
1001
1002    /// Set the foreground color.
1003    pub const fn fg(mut self, color: Color) -> Self {
1004        self.fg = Some(color);
1005        self
1006    }
1007
1008    /// Set the background color.
1009    pub const fn bg(mut self, color: Color) -> Self {
1010        self.bg = Some(color);
1011        self
1012    }
1013
1014    /// Add the bold modifier.
1015    pub fn bold(mut self) -> Self {
1016        self.modifiers |= Modifiers::BOLD;
1017        self
1018    }
1019
1020    /// Add the dim modifier.
1021    pub fn dim(mut self) -> Self {
1022        self.modifiers |= Modifiers::DIM;
1023        self
1024    }
1025
1026    /// Add the italic modifier.
1027    pub fn italic(mut self) -> Self {
1028        self.modifiers |= Modifiers::ITALIC;
1029        self
1030    }
1031
1032    /// Add the underline modifier.
1033    pub fn underline(mut self) -> Self {
1034        self.modifiers |= Modifiers::UNDERLINE;
1035        self
1036    }
1037
1038    /// Add the reversed (inverted colors) modifier.
1039    pub fn reversed(mut self) -> Self {
1040        self.modifiers |= Modifiers::REVERSED;
1041        self
1042    }
1043
1044    /// Add the strikethrough modifier.
1045    pub fn strikethrough(mut self) -> Self {
1046        self.modifiers |= Modifiers::STRIKETHROUGH;
1047        self
1048    }
1049}