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
149fn xterm256_to_rgb(idx: u8) -> (u8, u8, u8) {
150    match idx {
151        0 => (0, 0, 0),
152        1 => (128, 0, 0),
153        2 => (0, 128, 0),
154        3 => (128, 128, 0),
155        4 => (0, 0, 128),
156        5 => (128, 0, 128),
157        6 => (0, 128, 128),
158        7 => (192, 192, 192),
159        8 => (128, 128, 128),
160        9 => (255, 0, 0),
161        10 => (0, 255, 0),
162        11 => (255, 255, 0),
163        12 => (0, 0, 255),
164        13 => (255, 0, 255),
165        14 => (0, 255, 255),
166        15 => (255, 255, 255),
167        16..=231 => {
168            let n = idx - 16;
169            let b_idx = n % 6;
170            let g_idx = (n / 6) % 6;
171            let r_idx = n / 36;
172            let to_val = |i: u8| if i == 0 { 0u8 } else { 55 + 40 * i };
173            (to_val(r_idx), to_val(g_idx), to_val(b_idx))
174        }
175        232..=255 => {
176            let v = 8 + 10 * (idx - 232);
177            (v, v, v)
178        }
179    }
180}
181
182/// A color theme that flows through all widgets automatically.
183///
184/// Construct with [`Theme::dark()`] or [`Theme::light()`], or build a custom
185/// theme by filling in the fields directly. Pass the theme via [`crate::RunConfig`]
186/// and every widget will pick up the colors without any extra wiring.
187#[derive(Debug, Clone, Copy)]
188#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
189pub struct Theme {
190    /// Primary accent color, used for focused borders and highlights.
191    pub primary: Color,
192    /// Secondary accent color, used for less prominent highlights.
193    pub secondary: Color,
194    /// Accent color for decorative elements.
195    pub accent: Color,
196    /// Default foreground text color.
197    pub text: Color,
198    /// Dimmed text color for secondary labels and hints.
199    pub text_dim: Color,
200    /// Border color for unfocused containers.
201    pub border: Color,
202    /// Background color. Typically [`Color::Reset`] to inherit the terminal background.
203    pub bg: Color,
204    /// Color for success states (e.g., toast notifications).
205    pub success: Color,
206    /// Color for warning states.
207    pub warning: Color,
208    /// Color for error states.
209    pub error: Color,
210    /// Background color for selected list/table rows.
211    pub selected_bg: Color,
212    /// Foreground color for selected list/table rows.
213    pub selected_fg: Color,
214    /// Subtle surface color for card backgrounds and elevated containers.
215    pub surface: Color,
216    /// Hover/active surface color, one step brighter than `surface`.
217    ///
218    /// Used for interactive element hover states. Should be visually
219    /// distinguishable from both `surface` and `border`.
220    pub surface_hover: Color,
221    /// Secondary text color guaranteed readable on `surface` backgrounds.
222    ///
223    /// Use this instead of `text_dim` when rendering on `surface`-colored
224    /// containers. `text_dim` is tuned for the main `bg`; on `surface` it
225    /// may lack contrast.
226    pub surface_text: Color,
227}
228
229impl Theme {
230    /// Create a dark theme with cyan primary and white text.
231    pub fn dark() -> Self {
232        Self {
233            primary: Color::Cyan,
234            secondary: Color::Blue,
235            accent: Color::Magenta,
236            text: Color::White,
237            text_dim: Color::Indexed(245),
238            border: Color::Indexed(240),
239            bg: Color::Reset,
240            success: Color::Green,
241            warning: Color::Yellow,
242            error: Color::Red,
243            selected_bg: Color::Cyan,
244            selected_fg: Color::Black,
245            surface: Color::Indexed(236),
246            surface_hover: Color::Indexed(238),
247            surface_text: Color::Indexed(250),
248        }
249    }
250
251    /// Create a light theme with blue primary and black text.
252    pub fn light() -> Self {
253        Self {
254            primary: Color::Blue,
255            secondary: Color::Cyan,
256            accent: Color::Magenta,
257            text: Color::Black,
258            text_dim: Color::Indexed(240),
259            border: Color::Indexed(245),
260            bg: Color::Reset,
261            success: Color::Green,
262            warning: Color::Yellow,
263            error: Color::Red,
264            selected_bg: Color::Blue,
265            selected_fg: Color::White,
266            surface: Color::Indexed(254),
267            surface_hover: Color::Indexed(252),
268            surface_text: Color::Indexed(238),
269        }
270    }
271
272    /// Dracula theme — purple primary on dark gray.
273    pub fn dracula() -> Self {
274        Self {
275            primary: Color::Rgb(189, 147, 249),
276            secondary: Color::Rgb(139, 233, 253),
277            accent: Color::Rgb(255, 121, 198),
278            text: Color::Rgb(248, 248, 242),
279            text_dim: Color::Rgb(98, 114, 164),
280            border: Color::Rgb(68, 71, 90),
281            bg: Color::Rgb(40, 42, 54),
282            success: Color::Rgb(80, 250, 123),
283            warning: Color::Rgb(241, 250, 140),
284            error: Color::Rgb(255, 85, 85),
285            selected_bg: Color::Rgb(189, 147, 249),
286            selected_fg: Color::Rgb(40, 42, 54),
287            surface: Color::Rgb(68, 71, 90),
288            surface_hover: Color::Rgb(98, 100, 120),
289            surface_text: Color::Rgb(191, 194, 210),
290        }
291    }
292
293    /// Catppuccin Mocha theme — lavender primary on dark base.
294    pub fn catppuccin() -> Self {
295        Self {
296            primary: Color::Rgb(180, 190, 254),
297            secondary: Color::Rgb(137, 180, 250),
298            accent: Color::Rgb(245, 194, 231),
299            text: Color::Rgb(205, 214, 244),
300            text_dim: Color::Rgb(127, 132, 156),
301            border: Color::Rgb(88, 91, 112),
302            bg: Color::Rgb(30, 30, 46),
303            success: Color::Rgb(166, 227, 161),
304            warning: Color::Rgb(249, 226, 175),
305            error: Color::Rgb(243, 139, 168),
306            selected_bg: Color::Rgb(180, 190, 254),
307            selected_fg: Color::Rgb(30, 30, 46),
308            surface: Color::Rgb(49, 50, 68),
309            surface_hover: Color::Rgb(69, 71, 90),
310            surface_text: Color::Rgb(166, 173, 200),
311        }
312    }
313
314    /// Nord theme — frost blue primary on polar night.
315    pub fn nord() -> Self {
316        Self {
317            primary: Color::Rgb(136, 192, 208),
318            secondary: Color::Rgb(129, 161, 193),
319            accent: Color::Rgb(180, 142, 173),
320            text: Color::Rgb(236, 239, 244),
321            text_dim: Color::Rgb(76, 86, 106),
322            border: Color::Rgb(76, 86, 106),
323            bg: Color::Rgb(46, 52, 64),
324            success: Color::Rgb(163, 190, 140),
325            warning: Color::Rgb(235, 203, 139),
326            error: Color::Rgb(191, 97, 106),
327            selected_bg: Color::Rgb(136, 192, 208),
328            selected_fg: Color::Rgb(46, 52, 64),
329            surface: Color::Rgb(59, 66, 82),
330            surface_hover: Color::Rgb(67, 76, 94),
331            surface_text: Color::Rgb(216, 222, 233),
332        }
333    }
334
335    /// Solarized Dark theme — blue primary on dark base.
336    pub fn solarized_dark() -> Self {
337        Self {
338            primary: Color::Rgb(38, 139, 210),
339            secondary: Color::Rgb(42, 161, 152),
340            accent: Color::Rgb(211, 54, 130),
341            text: Color::Rgb(131, 148, 150),
342            text_dim: Color::Rgb(88, 110, 117),
343            border: Color::Rgb(88, 110, 117),
344            bg: Color::Rgb(0, 43, 54),
345            success: Color::Rgb(133, 153, 0),
346            warning: Color::Rgb(181, 137, 0),
347            error: Color::Rgb(220, 50, 47),
348            selected_bg: Color::Rgb(38, 139, 210),
349            selected_fg: Color::Rgb(253, 246, 227),
350            surface: Color::Rgb(7, 54, 66),
351            surface_hover: Color::Rgb(23, 72, 85),
352            surface_text: Color::Rgb(147, 161, 161),
353        }
354    }
355
356    /// Tokyo Night theme — blue primary on dark storm base.
357    pub fn tokyo_night() -> Self {
358        Self {
359            primary: Color::Rgb(122, 162, 247),
360            secondary: Color::Rgb(125, 207, 255),
361            accent: Color::Rgb(187, 154, 247),
362            text: Color::Rgb(169, 177, 214),
363            text_dim: Color::Rgb(86, 95, 137),
364            border: Color::Rgb(54, 58, 79),
365            bg: Color::Rgb(26, 27, 38),
366            success: Color::Rgb(158, 206, 106),
367            warning: Color::Rgb(224, 175, 104),
368            error: Color::Rgb(247, 118, 142),
369            selected_bg: Color::Rgb(122, 162, 247),
370            selected_fg: Color::Rgb(26, 27, 38),
371            surface: Color::Rgb(36, 40, 59),
372            surface_hover: Color::Rgb(41, 46, 66),
373            surface_text: Color::Rgb(192, 202, 245),
374        }
375    }
376}
377
378impl Default for Theme {
379    fn default() -> Self {
380        Self::dark()
381    }
382}
383
384/// Border style for containers.
385///
386/// Pass to `Context::bordered()` to draw a box around a container.
387/// Each variant uses a different set of Unicode box-drawing characters.
388#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
389#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
390pub enum Border {
391    /// Single-line box: `┌─┐│└─┘`
392    Single,
393    /// Double-line box: `╔═╗║╚═╝`
394    Double,
395    /// Rounded corners: `╭─╮│╰─╯`
396    Rounded,
397    /// Thick single-line box: `┏━┓┃┗━┛`
398    Thick,
399}
400
401/// Character set for a specific border style.
402///
403/// Returned by [`Border::chars`]. Contains the six box-drawing characters
404/// needed to render a complete border: four corners and two line segments.
405#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
406pub struct BorderChars {
407    /// Top-left corner character.
408    pub tl: char,
409    /// Top-right corner character.
410    pub tr: char,
411    /// Bottom-left corner character.
412    pub bl: char,
413    /// Bottom-right corner character.
414    pub br: char,
415    /// Horizontal line character.
416    pub h: char,
417    /// Vertical line character.
418    pub v: char,
419}
420
421/// Controls which sides of a border are visible.
422#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
423#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
424pub struct BorderSides {
425    pub top: bool,
426    pub right: bool,
427    pub bottom: bool,
428    pub left: bool,
429}
430
431impl BorderSides {
432    pub const fn all() -> Self {
433        Self {
434            top: true,
435            right: true,
436            bottom: true,
437            left: true,
438        }
439    }
440
441    pub const fn none() -> Self {
442        Self {
443            top: false,
444            right: false,
445            bottom: false,
446            left: false,
447        }
448    }
449
450    pub const fn horizontal() -> Self {
451        Self {
452            top: true,
453            right: false,
454            bottom: true,
455            left: false,
456        }
457    }
458
459    pub const fn vertical() -> Self {
460        Self {
461            top: false,
462            right: true,
463            bottom: false,
464            left: true,
465        }
466    }
467
468    pub fn has_horizontal(&self) -> bool {
469        self.top || self.bottom
470    }
471
472    pub fn has_vertical(&self) -> bool {
473        self.left || self.right
474    }
475}
476
477impl Default for BorderSides {
478    fn default() -> Self {
479        Self::all()
480    }
481}
482
483impl Border {
484    /// Return the [`BorderChars`] for this border style.
485    pub const fn chars(self) -> BorderChars {
486        match self {
487            Self::Single => BorderChars {
488                tl: '┌',
489                tr: '┐',
490                bl: '└',
491                br: '┘',
492                h: '─',
493                v: '│',
494            },
495            Self::Double => BorderChars {
496                tl: '╔',
497                tr: '╗',
498                bl: '╚',
499                br: '╝',
500                h: '═',
501                v: '║',
502            },
503            Self::Rounded => BorderChars {
504                tl: '╭',
505                tr: '╮',
506                bl: '╰',
507                br: '╯',
508                h: '─',
509                v: '│',
510            },
511            Self::Thick => BorderChars {
512                tl: '┏',
513                tr: '┓',
514                bl: '┗',
515                br: '┛',
516                h: '━',
517                v: '┃',
518            },
519        }
520    }
521}
522
523/// Padding inside a container border.
524///
525/// Shrinks the content area inward from each edge. All values are in terminal
526/// columns/rows.
527#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
528#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
529pub struct Padding {
530    /// Padding on the top edge.
531    pub top: u32,
532    /// Padding on the right edge.
533    pub right: u32,
534    /// Padding on the bottom edge.
535    pub bottom: u32,
536    /// Padding on the left edge.
537    pub left: u32,
538}
539
540impl Padding {
541    /// Create uniform padding on all four sides.
542    pub const fn all(v: u32) -> Self {
543        Self::new(v, v, v, v)
544    }
545
546    /// Create padding with `x` on left/right and `y` on top/bottom.
547    pub const fn xy(x: u32, y: u32) -> Self {
548        Self::new(y, x, y, x)
549    }
550
551    /// Create padding with explicit values for each side.
552    pub const fn new(top: u32, right: u32, bottom: u32, left: u32) -> Self {
553        Self {
554            top,
555            right,
556            bottom,
557            left,
558        }
559    }
560
561    /// Total horizontal padding (`left + right`).
562    pub const fn horizontal(self) -> u32 {
563        self.left + self.right
564    }
565
566    /// Total vertical padding (`top + bottom`).
567    pub const fn vertical(self) -> u32 {
568        self.top + self.bottom
569    }
570}
571
572/// Margin outside a container.
573///
574/// Adds space around the outside of a container's border. All values are in
575/// terminal columns/rows.
576#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
577#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
578pub struct Margin {
579    /// Margin on the top edge.
580    pub top: u32,
581    /// Margin on the right edge.
582    pub right: u32,
583    /// Margin on the bottom edge.
584    pub bottom: u32,
585    /// Margin on the left edge.
586    pub left: u32,
587}
588
589impl Margin {
590    /// Create uniform margin on all four sides.
591    pub const fn all(v: u32) -> Self {
592        Self::new(v, v, v, v)
593    }
594
595    /// Create margin with `x` on left/right and `y` on top/bottom.
596    pub const fn xy(x: u32, y: u32) -> Self {
597        Self::new(y, x, y, x)
598    }
599
600    /// Create margin with explicit values for each side.
601    pub const fn new(top: u32, right: u32, bottom: u32, left: u32) -> Self {
602        Self {
603            top,
604            right,
605            bottom,
606            left,
607        }
608    }
609
610    /// Total horizontal margin (`left + right`).
611    pub const fn horizontal(self) -> u32 {
612        self.left + self.right
613    }
614
615    /// Total vertical margin (`top + bottom`).
616    pub const fn vertical(self) -> u32 {
617        self.top + self.bottom
618    }
619}
620
621/// Size constraints for layout computation.
622///
623/// All fields are optional. Unset constraints are unconstrained. Use the
624/// builder methods to set individual bounds in a fluent style.
625///
626/// # Example
627///
628/// ```
629/// use slt::Constraints;
630///
631/// let c = Constraints::default().min_w(10).max_w(40);
632/// ```
633#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
634#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
635#[must_use = "configure constraints using the returned value"]
636pub struct Constraints {
637    /// Minimum width in terminal columns, if any.
638    pub min_width: Option<u32>,
639    /// Maximum width in terminal columns, if any.
640    pub max_width: Option<u32>,
641    /// Minimum height in terminal rows, if any.
642    pub min_height: Option<u32>,
643    /// Maximum height in terminal rows, if any.
644    pub max_height: Option<u32>,
645    /// Width as a percentage (1-100) of the parent container.
646    pub width_pct: Option<u8>,
647    /// Height as a percentage (1-100) of the parent container.
648    pub height_pct: Option<u8>,
649}
650
651impl Constraints {
652    /// Set the minimum width constraint.
653    pub const fn min_w(mut self, min_width: u32) -> Self {
654        self.min_width = Some(min_width);
655        self
656    }
657
658    /// Set the maximum width constraint.
659    pub const fn max_w(mut self, max_width: u32) -> Self {
660        self.max_width = Some(max_width);
661        self
662    }
663
664    /// Set the minimum height constraint.
665    pub const fn min_h(mut self, min_height: u32) -> Self {
666        self.min_height = Some(min_height);
667        self
668    }
669
670    /// Set the maximum height constraint.
671    pub const fn max_h(mut self, max_height: u32) -> Self {
672        self.max_height = Some(max_height);
673        self
674    }
675
676    /// Set width as a percentage (1-100) of the parent container.
677    pub const fn w_pct(mut self, pct: u8) -> Self {
678        self.width_pct = Some(pct);
679        self
680    }
681
682    /// Set height as a percentage (1-100) of the parent container.
683    pub const fn h_pct(mut self, pct: u8) -> Self {
684        self.height_pct = Some(pct);
685        self
686    }
687}
688
689/// Cross-axis alignment within a container.
690///
691/// Controls how children are positioned along the axis perpendicular to the
692/// container's main axis. For a `row()`, this is vertical alignment; for a
693/// `col()`, this is horizontal alignment.
694#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
695#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
696pub enum Align {
697    /// Align children to the start of the cross axis (default).
698    #[default]
699    Start,
700    /// Center children on the cross axis.
701    Center,
702    /// Align children to the end of the cross axis.
703    End,
704}
705
706/// Main-axis content distribution within a container.
707///
708/// Controls how children are distributed along the main axis. For a `row()`,
709/// this is horizontal distribution; for a `col()`, this is vertical.
710///
711/// When children have `grow > 0`, they consume remaining space before justify
712/// distribution applies. Justify modes only affect the leftover space after
713/// flex-grow allocation.
714#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
715#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
716pub enum Justify {
717    /// Pack children at the start (default). Uses `gap` for spacing.
718    #[default]
719    Start,
720    /// Center children along the main axis with `gap` spacing.
721    Center,
722    /// Pack children at the end with `gap` spacing.
723    End,
724    /// First child at start, last at end, equal space between.
725    SpaceBetween,
726    /// Equal space around each child (half-size space at edges).
727    SpaceAround,
728    /// Equal space between all children and at both edges.
729    SpaceEvenly,
730}
731
732/// Text modifier bitflags stored as a `u8`.
733///
734/// Combine modifiers with `|` or [`Modifiers::insert`]. Check membership with
735/// [`Modifiers::contains`].
736#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
737#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
738#[cfg_attr(feature = "serde", serde(transparent))]
739pub struct Modifiers(pub u8);
740
741impl Modifiers {
742    /// No modifiers set.
743    pub const NONE: Self = Self(0);
744    /// Bold text.
745    pub const BOLD: Self = Self(1 << 0);
746    /// Dimmed/faint text.
747    pub const DIM: Self = Self(1 << 1);
748    /// Italic text.
749    pub const ITALIC: Self = Self(1 << 2);
750    /// Underlined text.
751    pub const UNDERLINE: Self = Self(1 << 3);
752    /// Reversed foreground/background colors.
753    pub const REVERSED: Self = Self(1 << 4);
754    /// Strikethrough text.
755    pub const STRIKETHROUGH: Self = Self(1 << 5);
756
757    /// Returns `true` if all bits in `other` are set in `self`.
758    #[inline]
759    pub fn contains(self, other: Self) -> bool {
760        (self.0 & other.0) == other.0
761    }
762
763    /// Set all bits from `other` into `self`.
764    #[inline]
765    pub fn insert(&mut self, other: Self) {
766        self.0 |= other.0;
767    }
768
769    /// Returns `true` if no modifiers are set.
770    #[inline]
771    pub fn is_empty(self) -> bool {
772        self.0 == 0
773    }
774}
775
776impl std::ops::BitOr for Modifiers {
777    type Output = Self;
778    #[inline]
779    fn bitor(self, rhs: Self) -> Self {
780        Self(self.0 | rhs.0)
781    }
782}
783
784impl std::ops::BitOrAssign for Modifiers {
785    #[inline]
786    fn bitor_assign(&mut self, rhs: Self) {
787        self.0 |= rhs.0;
788    }
789}
790
791/// Visual style for a terminal cell (foreground, background, modifiers).
792///
793/// Styles are applied to text via the builder methods on `Context` widget
794/// calls (e.g., `.bold()`, `.fg(Color::Cyan)`). All fields are optional;
795/// `None` means "inherit from the terminal default."
796///
797/// # Example
798///
799/// ```
800/// use slt::{Style, Color};
801///
802/// let style = Style::new().fg(Color::Cyan).bold();
803/// ```
804#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
805#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
806#[must_use = "build and pass the returned Style value"]
807pub struct Style {
808    /// Foreground color, or `None` to use the terminal default.
809    pub fg: Option<Color>,
810    /// Background color, or `None` to use the terminal default.
811    pub bg: Option<Color>,
812    /// Text modifiers (bold, italic, underline, etc.).
813    pub modifiers: Modifiers,
814}
815
816impl Style {
817    /// Create a new style with no color or modifiers set.
818    pub const fn new() -> Self {
819        Self {
820            fg: None,
821            bg: None,
822            modifiers: Modifiers::NONE,
823        }
824    }
825
826    /// Set the foreground color.
827    pub const fn fg(mut self, color: Color) -> Self {
828        self.fg = Some(color);
829        self
830    }
831
832    /// Set the background color.
833    pub const fn bg(mut self, color: Color) -> Self {
834        self.bg = Some(color);
835        self
836    }
837
838    /// Add the bold modifier.
839    pub fn bold(mut self) -> Self {
840        self.modifiers |= Modifiers::BOLD;
841        self
842    }
843
844    /// Add the dim modifier.
845    pub fn dim(mut self) -> Self {
846        self.modifiers |= Modifiers::DIM;
847        self
848    }
849
850    /// Add the italic modifier.
851    pub fn italic(mut self) -> Self {
852        self.modifiers |= Modifiers::ITALIC;
853        self
854    }
855
856    /// Add the underline modifier.
857    pub fn underline(mut self) -> Self {
858        self.modifiers |= Modifiers::UNDERLINE;
859        self
860    }
861
862    /// Add the reversed (inverted colors) modifier.
863    pub fn reversed(mut self) -> Self {
864        self.modifiers |= Modifiers::REVERSED;
865        self
866    }
867
868    /// Add the strikethrough modifier.
869    pub fn strikethrough(mut self) -> Self {
870        self.modifiers |= Modifiers::STRIKETHROUGH;
871        self
872    }
873}