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    /// Create a [`ThemeBuilder`] for configuring a custom theme.
411    ///
412    /// # Example
413    ///
414    /// ```
415    /// use slt::{Color, Theme};
416    ///
417    /// let theme = Theme::builder()
418    ///     .primary(Color::Rgb(255, 107, 107))
419    ///     .accent(Color::Cyan)
420    ///     .build();
421    /// ```
422    pub fn builder() -> ThemeBuilder {
423        ThemeBuilder {
424            primary: None,
425            secondary: None,
426            accent: None,
427            text: None,
428            text_dim: None,
429            border: None,
430            bg: None,
431            success: None,
432            warning: None,
433            error: None,
434            selected_bg: None,
435            selected_fg: None,
436            surface: None,
437            surface_hover: None,
438            surface_text: None,
439        }
440    }
441
442    /// Dracula theme — purple primary on dark gray.
443    pub fn dracula() -> Self {
444        Self {
445            primary: Color::Rgb(189, 147, 249),
446            secondary: Color::Rgb(139, 233, 253),
447            accent: Color::Rgb(255, 121, 198),
448            text: Color::Rgb(248, 248, 242),
449            text_dim: Color::Rgb(98, 114, 164),
450            border: Color::Rgb(68, 71, 90),
451            bg: Color::Rgb(40, 42, 54),
452            success: Color::Rgb(80, 250, 123),
453            warning: Color::Rgb(241, 250, 140),
454            error: Color::Rgb(255, 85, 85),
455            selected_bg: Color::Rgb(189, 147, 249),
456            selected_fg: Color::Rgb(40, 42, 54),
457            surface: Color::Rgb(68, 71, 90),
458            surface_hover: Color::Rgb(98, 100, 120),
459            surface_text: Color::Rgb(191, 194, 210),
460        }
461    }
462
463    /// Catppuccin Mocha theme — lavender primary on dark base.
464    pub fn catppuccin() -> Self {
465        Self {
466            primary: Color::Rgb(180, 190, 254),
467            secondary: Color::Rgb(137, 180, 250),
468            accent: Color::Rgb(245, 194, 231),
469            text: Color::Rgb(205, 214, 244),
470            text_dim: Color::Rgb(127, 132, 156),
471            border: Color::Rgb(88, 91, 112),
472            bg: Color::Rgb(30, 30, 46),
473            success: Color::Rgb(166, 227, 161),
474            warning: Color::Rgb(249, 226, 175),
475            error: Color::Rgb(243, 139, 168),
476            selected_bg: Color::Rgb(180, 190, 254),
477            selected_fg: Color::Rgb(30, 30, 46),
478            surface: Color::Rgb(49, 50, 68),
479            surface_hover: Color::Rgb(69, 71, 90),
480            surface_text: Color::Rgb(166, 173, 200),
481        }
482    }
483
484    /// Nord theme — frost blue primary on polar night.
485    pub fn nord() -> Self {
486        Self {
487            primary: Color::Rgb(136, 192, 208),
488            secondary: Color::Rgb(129, 161, 193),
489            accent: Color::Rgb(180, 142, 173),
490            text: Color::Rgb(236, 239, 244),
491            text_dim: Color::Rgb(76, 86, 106),
492            border: Color::Rgb(76, 86, 106),
493            bg: Color::Rgb(46, 52, 64),
494            success: Color::Rgb(163, 190, 140),
495            warning: Color::Rgb(235, 203, 139),
496            error: Color::Rgb(191, 97, 106),
497            selected_bg: Color::Rgb(136, 192, 208),
498            selected_fg: Color::Rgb(46, 52, 64),
499            surface: Color::Rgb(59, 66, 82),
500            surface_hover: Color::Rgb(67, 76, 94),
501            surface_text: Color::Rgb(216, 222, 233),
502        }
503    }
504
505    /// Solarized Dark theme — blue primary on dark base.
506    pub fn solarized_dark() -> Self {
507        Self {
508            primary: Color::Rgb(38, 139, 210),
509            secondary: Color::Rgb(42, 161, 152),
510            accent: Color::Rgb(211, 54, 130),
511            text: Color::Rgb(131, 148, 150),
512            text_dim: Color::Rgb(88, 110, 117),
513            border: Color::Rgb(88, 110, 117),
514            bg: Color::Rgb(0, 43, 54),
515            success: Color::Rgb(133, 153, 0),
516            warning: Color::Rgb(181, 137, 0),
517            error: Color::Rgb(220, 50, 47),
518            selected_bg: Color::Rgb(38, 139, 210),
519            selected_fg: Color::Rgb(253, 246, 227),
520            surface: Color::Rgb(7, 54, 66),
521            surface_hover: Color::Rgb(23, 72, 85),
522            surface_text: Color::Rgb(147, 161, 161),
523        }
524    }
525
526    /// Tokyo Night theme — blue primary on dark storm base.
527    pub fn tokyo_night() -> Self {
528        Self {
529            primary: Color::Rgb(122, 162, 247),
530            secondary: Color::Rgb(125, 207, 255),
531            accent: Color::Rgb(187, 154, 247),
532            text: Color::Rgb(169, 177, 214),
533            text_dim: Color::Rgb(86, 95, 137),
534            border: Color::Rgb(54, 58, 79),
535            bg: Color::Rgb(26, 27, 38),
536            success: Color::Rgb(158, 206, 106),
537            warning: Color::Rgb(224, 175, 104),
538            error: Color::Rgb(247, 118, 142),
539            selected_bg: Color::Rgb(122, 162, 247),
540            selected_fg: Color::Rgb(26, 27, 38),
541            surface: Color::Rgb(36, 40, 59),
542            surface_hover: Color::Rgb(41, 46, 66),
543            surface_text: Color::Rgb(192, 202, 245),
544        }
545    }
546}
547
548/// Builder for creating custom themes with defaults from `Theme::dark()`.
549pub struct ThemeBuilder {
550    primary: Option<Color>,
551    secondary: Option<Color>,
552    accent: Option<Color>,
553    text: Option<Color>,
554    text_dim: Option<Color>,
555    border: Option<Color>,
556    bg: Option<Color>,
557    success: Option<Color>,
558    warning: Option<Color>,
559    error: Option<Color>,
560    selected_bg: Option<Color>,
561    selected_fg: Option<Color>,
562    surface: Option<Color>,
563    surface_hover: Option<Color>,
564    surface_text: Option<Color>,
565}
566
567impl ThemeBuilder {
568    pub fn primary(mut self, color: Color) -> Self {
569        self.primary = Some(color);
570        self
571    }
572
573    pub fn secondary(mut self, color: Color) -> Self {
574        self.secondary = Some(color);
575        self
576    }
577
578    pub fn accent(mut self, color: Color) -> Self {
579        self.accent = Some(color);
580        self
581    }
582
583    pub fn text(mut self, color: Color) -> Self {
584        self.text = Some(color);
585        self
586    }
587
588    pub fn text_dim(mut self, color: Color) -> Self {
589        self.text_dim = Some(color);
590        self
591    }
592
593    pub fn border(mut self, color: Color) -> Self {
594        self.border = Some(color);
595        self
596    }
597
598    pub fn bg(mut self, color: Color) -> Self {
599        self.bg = Some(color);
600        self
601    }
602
603    pub fn success(mut self, color: Color) -> Self {
604        self.success = Some(color);
605        self
606    }
607
608    pub fn warning(mut self, color: Color) -> Self {
609        self.warning = Some(color);
610        self
611    }
612
613    pub fn error(mut self, color: Color) -> Self {
614        self.error = Some(color);
615        self
616    }
617
618    pub fn selected_bg(mut self, color: Color) -> Self {
619        self.selected_bg = Some(color);
620        self
621    }
622
623    pub fn selected_fg(mut self, color: Color) -> Self {
624        self.selected_fg = Some(color);
625        self
626    }
627
628    pub fn surface(mut self, color: Color) -> Self {
629        self.surface = Some(color);
630        self
631    }
632
633    pub fn surface_hover(mut self, color: Color) -> Self {
634        self.surface_hover = Some(color);
635        self
636    }
637
638    pub fn surface_text(mut self, color: Color) -> Self {
639        self.surface_text = Some(color);
640        self
641    }
642
643    pub fn build(self) -> Theme {
644        let defaults = Theme::dark();
645        Theme {
646            primary: self.primary.unwrap_or(defaults.primary),
647            secondary: self.secondary.unwrap_or(defaults.secondary),
648            accent: self.accent.unwrap_or(defaults.accent),
649            text: self.text.unwrap_or(defaults.text),
650            text_dim: self.text_dim.unwrap_or(defaults.text_dim),
651            border: self.border.unwrap_or(defaults.border),
652            bg: self.bg.unwrap_or(defaults.bg),
653            success: self.success.unwrap_or(defaults.success),
654            warning: self.warning.unwrap_or(defaults.warning),
655            error: self.error.unwrap_or(defaults.error),
656            selected_bg: self.selected_bg.unwrap_or(defaults.selected_bg),
657            selected_fg: self.selected_fg.unwrap_or(defaults.selected_fg),
658            surface: self.surface.unwrap_or(defaults.surface),
659            surface_hover: self.surface_hover.unwrap_or(defaults.surface_hover),
660            surface_text: self.surface_text.unwrap_or(defaults.surface_text),
661        }
662    }
663}
664
665impl Default for Theme {
666    fn default() -> Self {
667        Self::dark()
668    }
669}
670
671/// Terminal size breakpoint for responsive layouts.
672///
673/// Based on the current terminal width. Use [`Context::breakpoint`] to
674/// get the active breakpoint.
675#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
676pub enum Breakpoint {
677    /// Width < 40 columns (phone-sized)
678    Xs,
679    /// Width 40-79 columns (small terminal)
680    Sm,
681    /// Width 80-119 columns (standard terminal)
682    Md,
683    /// Width 120-159 columns (wide terminal)
684    Lg,
685    /// Width >= 160 columns (ultra-wide)
686    Xl,
687}
688
689/// Border style for containers.
690///
691/// Pass to `Context::bordered()` to draw a box around a container.
692/// Each variant uses a different set of Unicode box-drawing characters.
693#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
694#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
695pub enum Border {
696    /// Single-line box: `┌─┐│└─┘`
697    Single,
698    /// Double-line box: `╔═╗║╚═╝`
699    Double,
700    /// Rounded corners: `╭─╮│╰─╯`
701    Rounded,
702    /// Thick single-line box: `┏━┓┃┗━┛`
703    Thick,
704    /// Dashed border using light dash characters: ┄╌┄╌
705    Dashed,
706    /// Heavy dashed border: ┅╍┅╍
707    DashedThick,
708}
709
710/// Character set for a specific border style.
711///
712/// Returned by [`Border::chars`]. Contains the six box-drawing characters
713/// needed to render a complete border: four corners and two line segments.
714#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
715pub struct BorderChars {
716    /// Top-left corner character.
717    pub tl: char,
718    /// Top-right corner character.
719    pub tr: char,
720    /// Bottom-left corner character.
721    pub bl: char,
722    /// Bottom-right corner character.
723    pub br: char,
724    /// Horizontal line character.
725    pub h: char,
726    /// Vertical line character.
727    pub v: char,
728}
729
730/// Controls which sides of a border are visible.
731#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
732#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
733pub struct BorderSides {
734    pub top: bool,
735    pub right: bool,
736    pub bottom: bool,
737    pub left: bool,
738}
739
740impl BorderSides {
741    pub const fn all() -> Self {
742        Self {
743            top: true,
744            right: true,
745            bottom: true,
746            left: true,
747        }
748    }
749
750    pub const fn none() -> Self {
751        Self {
752            top: false,
753            right: false,
754            bottom: false,
755            left: false,
756        }
757    }
758
759    pub const fn horizontal() -> Self {
760        Self {
761            top: true,
762            right: false,
763            bottom: true,
764            left: false,
765        }
766    }
767
768    pub const fn vertical() -> Self {
769        Self {
770            top: false,
771            right: true,
772            bottom: false,
773            left: true,
774        }
775    }
776
777    pub fn has_horizontal(&self) -> bool {
778        self.top || self.bottom
779    }
780
781    pub fn has_vertical(&self) -> bool {
782        self.left || self.right
783    }
784}
785
786impl Default for BorderSides {
787    fn default() -> Self {
788        Self::all()
789    }
790}
791
792impl Border {
793    /// Return the [`BorderChars`] for this border style.
794    pub const fn chars(self) -> BorderChars {
795        match self {
796            Self::Single => BorderChars {
797                tl: '┌',
798                tr: '┐',
799                bl: '└',
800                br: '┘',
801                h: '─',
802                v: '│',
803            },
804            Self::Double => BorderChars {
805                tl: '╔',
806                tr: '╗',
807                bl: '╚',
808                br: '╝',
809                h: '═',
810                v: '║',
811            },
812            Self::Rounded => BorderChars {
813                tl: '╭',
814                tr: '╮',
815                bl: '╰',
816                br: '╯',
817                h: '─',
818                v: '│',
819            },
820            Self::Thick => BorderChars {
821                tl: '┏',
822                tr: '┓',
823                bl: '┗',
824                br: '┛',
825                h: '━',
826                v: '┃',
827            },
828            Self::Dashed => BorderChars {
829                tl: '┌',
830                tr: '┐',
831                bl: '└',
832                br: '┘',
833                h: '┄',
834                v: '┆',
835            },
836            Self::DashedThick => BorderChars {
837                tl: '┏',
838                tr: '┓',
839                bl: '┗',
840                br: '┛',
841                h: '┅',
842                v: '┇',
843            },
844        }
845    }
846}
847
848/// Padding inside a container border.
849///
850/// Shrinks the content area inward from each edge. All values are in terminal
851/// columns/rows.
852#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
853#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
854pub struct Padding {
855    /// Padding on the top edge.
856    pub top: u32,
857    /// Padding on the right edge.
858    pub right: u32,
859    /// Padding on the bottom edge.
860    pub bottom: u32,
861    /// Padding on the left edge.
862    pub left: u32,
863}
864
865impl Padding {
866    /// Create uniform padding on all four sides.
867    pub const fn all(v: u32) -> Self {
868        Self::new(v, v, v, v)
869    }
870
871    /// Create padding with `x` on left/right and `y` on top/bottom.
872    pub const fn xy(x: u32, y: u32) -> Self {
873        Self::new(y, x, y, x)
874    }
875
876    /// Create padding with explicit values for each side.
877    pub const fn new(top: u32, right: u32, bottom: u32, left: u32) -> Self {
878        Self {
879            top,
880            right,
881            bottom,
882            left,
883        }
884    }
885
886    /// Total horizontal padding (`left + right`).
887    pub const fn horizontal(self) -> u32 {
888        self.left + self.right
889    }
890
891    /// Total vertical padding (`top + bottom`).
892    pub const fn vertical(self) -> u32 {
893        self.top + self.bottom
894    }
895}
896
897/// Margin outside a container.
898///
899/// Adds space around the outside of a container's border. All values are in
900/// terminal columns/rows.
901#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
902#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
903pub struct Margin {
904    /// Margin on the top edge.
905    pub top: u32,
906    /// Margin on the right edge.
907    pub right: u32,
908    /// Margin on the bottom edge.
909    pub bottom: u32,
910    /// Margin on the left edge.
911    pub left: u32,
912}
913
914impl Margin {
915    /// Create uniform margin on all four sides.
916    pub const fn all(v: u32) -> Self {
917        Self::new(v, v, v, v)
918    }
919
920    /// Create margin with `x` on left/right and `y` on top/bottom.
921    pub const fn xy(x: u32, y: u32) -> Self {
922        Self::new(y, x, y, x)
923    }
924
925    /// Create margin with explicit values for each side.
926    pub const fn new(top: u32, right: u32, bottom: u32, left: u32) -> Self {
927        Self {
928            top,
929            right,
930            bottom,
931            left,
932        }
933    }
934
935    /// Total horizontal margin (`left + right`).
936    pub const fn horizontal(self) -> u32 {
937        self.left + self.right
938    }
939
940    /// Total vertical margin (`top + bottom`).
941    pub const fn vertical(self) -> u32 {
942        self.top + self.bottom
943    }
944}
945
946/// Size constraints for layout computation.
947///
948/// All fields are optional. Unset constraints are unconstrained. Use the
949/// builder methods to set individual bounds in a fluent style.
950///
951/// # Example
952///
953/// ```
954/// use slt::Constraints;
955///
956/// let c = Constraints::default().min_w(10).max_w(40);
957/// ```
958#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
959#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
960#[must_use = "configure constraints using the returned value"]
961pub struct Constraints {
962    /// Minimum width in terminal columns, if any.
963    pub min_width: Option<u32>,
964    /// Maximum width in terminal columns, if any.
965    pub max_width: Option<u32>,
966    /// Minimum height in terminal rows, if any.
967    pub min_height: Option<u32>,
968    /// Maximum height in terminal rows, if any.
969    pub max_height: Option<u32>,
970    /// Width as a percentage (1-100) of the parent container.
971    pub width_pct: Option<u8>,
972    /// Height as a percentage (1-100) of the parent container.
973    pub height_pct: Option<u8>,
974}
975
976impl Constraints {
977    /// Set the minimum width constraint.
978    pub const fn min_w(mut self, min_width: u32) -> Self {
979        self.min_width = Some(min_width);
980        self
981    }
982
983    /// Set the maximum width constraint.
984    pub const fn max_w(mut self, max_width: u32) -> Self {
985        self.max_width = Some(max_width);
986        self
987    }
988
989    /// Set the minimum height constraint.
990    pub const fn min_h(mut self, min_height: u32) -> Self {
991        self.min_height = Some(min_height);
992        self
993    }
994
995    /// Set the maximum height constraint.
996    pub const fn max_h(mut self, max_height: u32) -> Self {
997        self.max_height = Some(max_height);
998        self
999    }
1000
1001    /// Set width as a percentage (1-100) of the parent container.
1002    pub const fn w_pct(mut self, pct: u8) -> Self {
1003        self.width_pct = Some(pct);
1004        self
1005    }
1006
1007    /// Set height as a percentage (1-100) of the parent container.
1008    pub const fn h_pct(mut self, pct: u8) -> Self {
1009        self.height_pct = Some(pct);
1010        self
1011    }
1012}
1013
1014/// Cross-axis alignment within a container.
1015///
1016/// Controls how children are positioned along the axis perpendicular to the
1017/// container's main axis. For a `row()`, this is vertical alignment; for a
1018/// `col()`, this is horizontal alignment.
1019#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
1020#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1021pub enum Align {
1022    /// Align children to the start of the cross axis (default).
1023    #[default]
1024    Start,
1025    /// Center children on the cross axis.
1026    Center,
1027    /// Align children to the end of the cross axis.
1028    End,
1029}
1030
1031/// Main-axis content distribution within a container.
1032///
1033/// Controls how children are distributed along the main axis. For a `row()`,
1034/// this is horizontal distribution; for a `col()`, this is vertical.
1035///
1036/// When children have `grow > 0`, they consume remaining space before justify
1037/// distribution applies. Justify modes only affect the leftover space after
1038/// flex-grow allocation.
1039#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
1040#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1041pub enum Justify {
1042    /// Pack children at the start (default). Uses `gap` for spacing.
1043    #[default]
1044    Start,
1045    /// Center children along the main axis with `gap` spacing.
1046    Center,
1047    /// Pack children at the end with `gap` spacing.
1048    End,
1049    /// First child at start, last at end, equal space between.
1050    SpaceBetween,
1051    /// Equal space around each child (half-size space at edges).
1052    SpaceAround,
1053    /// Equal space between all children and at both edges.
1054    SpaceEvenly,
1055}
1056
1057/// Text modifier bitflags stored as a `u8`.
1058///
1059/// Combine modifiers with `|` or [`Modifiers::insert`]. Check membership with
1060/// [`Modifiers::contains`].
1061#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
1062#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1063#[cfg_attr(feature = "serde", serde(transparent))]
1064pub struct Modifiers(pub u8);
1065
1066impl Modifiers {
1067    /// No modifiers set.
1068    pub const NONE: Self = Self(0);
1069    /// Bold text.
1070    pub const BOLD: Self = Self(1 << 0);
1071    /// Dimmed/faint text.
1072    pub const DIM: Self = Self(1 << 1);
1073    /// Italic text.
1074    pub const ITALIC: Self = Self(1 << 2);
1075    /// Underlined text.
1076    pub const UNDERLINE: Self = Self(1 << 3);
1077    /// Reversed foreground/background colors.
1078    pub const REVERSED: Self = Self(1 << 4);
1079    /// Strikethrough text.
1080    pub const STRIKETHROUGH: Self = Self(1 << 5);
1081
1082    /// Returns `true` if all bits in `other` are set in `self`.
1083    #[inline]
1084    pub fn contains(self, other: Self) -> bool {
1085        (self.0 & other.0) == other.0
1086    }
1087
1088    /// Set all bits from `other` into `self`.
1089    #[inline]
1090    pub fn insert(&mut self, other: Self) {
1091        self.0 |= other.0;
1092    }
1093
1094    /// Returns `true` if no modifiers are set.
1095    #[inline]
1096    pub fn is_empty(self) -> bool {
1097        self.0 == 0
1098    }
1099}
1100
1101impl std::ops::BitOr for Modifiers {
1102    type Output = Self;
1103    #[inline]
1104    fn bitor(self, rhs: Self) -> Self {
1105        Self(self.0 | rhs.0)
1106    }
1107}
1108
1109impl std::ops::BitOrAssign for Modifiers {
1110    #[inline]
1111    fn bitor_assign(&mut self, rhs: Self) {
1112        self.0 |= rhs.0;
1113    }
1114}
1115
1116/// Visual style for a terminal cell (foreground, background, modifiers).
1117///
1118/// Styles are applied to text via the builder methods on `Context` widget
1119/// calls (e.g., `.bold()`, `.fg(Color::Cyan)`). All fields are optional;
1120/// `None` means "inherit from the terminal default."
1121///
1122/// # Example
1123///
1124/// ```
1125/// use slt::{Style, Color};
1126///
1127/// let style = Style::new().fg(Color::Cyan).bold();
1128/// ```
1129#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
1130#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1131#[must_use = "build and pass the returned Style value"]
1132pub struct Style {
1133    /// Foreground color, or `None` to use the terminal default.
1134    pub fg: Option<Color>,
1135    /// Background color, or `None` to use the terminal default.
1136    pub bg: Option<Color>,
1137    /// Text modifiers (bold, italic, underline, etc.).
1138    pub modifiers: Modifiers,
1139}
1140
1141impl Style {
1142    /// Create a new style with no color or modifiers set.
1143    pub const fn new() -> Self {
1144        Self {
1145            fg: None,
1146            bg: None,
1147            modifiers: Modifiers::NONE,
1148        }
1149    }
1150
1151    /// Set the foreground color.
1152    pub const fn fg(mut self, color: Color) -> Self {
1153        self.fg = Some(color);
1154        self
1155    }
1156
1157    /// Set the background color.
1158    pub const fn bg(mut self, color: Color) -> Self {
1159        self.bg = Some(color);
1160        self
1161    }
1162
1163    /// Add the bold modifier.
1164    pub fn bold(mut self) -> Self {
1165        self.modifiers |= Modifiers::BOLD;
1166        self
1167    }
1168
1169    /// Add the dim modifier.
1170    pub fn dim(mut self) -> Self {
1171        self.modifiers |= Modifiers::DIM;
1172        self
1173    }
1174
1175    /// Add the italic modifier.
1176    pub fn italic(mut self) -> Self {
1177        self.modifiers |= Modifiers::ITALIC;
1178        self
1179    }
1180
1181    /// Add the underline modifier.
1182    pub fn underline(mut self) -> Self {
1183        self.modifiers |= Modifiers::UNDERLINE;
1184        self
1185    }
1186
1187    /// Add the reversed (inverted colors) modifier.
1188    pub fn reversed(mut self) -> Self {
1189        self.modifiers |= Modifiers::REVERSED;
1190        self
1191    }
1192
1193    /// Add the strikethrough modifier.
1194    pub fn strikethrough(mut self) -> Self {
1195        self.modifiers |= Modifiers::STRIKETHROUGH;
1196        self
1197    }
1198}