Skip to main content

slt/
style.rs

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