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
421impl Border {
422    /// Return the [`BorderChars`] for this border style.
423    pub const fn chars(self) -> BorderChars {
424        match self {
425            Self::Single => BorderChars {
426                tl: '┌',
427                tr: '┐',
428                bl: '└',
429                br: '┘',
430                h: '─',
431                v: '│',
432            },
433            Self::Double => BorderChars {
434                tl: '╔',
435                tr: '╗',
436                bl: '╚',
437                br: '╝',
438                h: '═',
439                v: '║',
440            },
441            Self::Rounded => BorderChars {
442                tl: '╭',
443                tr: '╮',
444                bl: '╰',
445                br: '╯',
446                h: '─',
447                v: '│',
448            },
449            Self::Thick => BorderChars {
450                tl: '┏',
451                tr: '┓',
452                bl: '┗',
453                br: '┛',
454                h: '━',
455                v: '┃',
456            },
457        }
458    }
459}
460
461/// Padding inside a container border.
462///
463/// Shrinks the content area inward from each edge. All values are in terminal
464/// columns/rows.
465#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
466#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
467pub struct Padding {
468    /// Padding on the top edge.
469    pub top: u32,
470    /// Padding on the right edge.
471    pub right: u32,
472    /// Padding on the bottom edge.
473    pub bottom: u32,
474    /// Padding on the left edge.
475    pub left: u32,
476}
477
478impl Padding {
479    /// Create uniform padding on all four sides.
480    pub const fn all(v: u32) -> Self {
481        Self::new(v, v, v, v)
482    }
483
484    /// Create padding with `x` on left/right and `y` on top/bottom.
485    pub const fn xy(x: u32, y: u32) -> Self {
486        Self::new(y, x, y, x)
487    }
488
489    /// Create padding with explicit values for each side.
490    pub const fn new(top: u32, right: u32, bottom: u32, left: u32) -> Self {
491        Self {
492            top,
493            right,
494            bottom,
495            left,
496        }
497    }
498
499    /// Total horizontal padding (`left + right`).
500    pub const fn horizontal(self) -> u32 {
501        self.left + self.right
502    }
503
504    /// Total vertical padding (`top + bottom`).
505    pub const fn vertical(self) -> u32 {
506        self.top + self.bottom
507    }
508}
509
510/// Margin outside a container.
511///
512/// Adds space around the outside of a container's border. All values are in
513/// terminal columns/rows.
514#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
515#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
516pub struct Margin {
517    /// Margin on the top edge.
518    pub top: u32,
519    /// Margin on the right edge.
520    pub right: u32,
521    /// Margin on the bottom edge.
522    pub bottom: u32,
523    /// Margin on the left edge.
524    pub left: u32,
525}
526
527impl Margin {
528    /// Create uniform margin on all four sides.
529    pub const fn all(v: u32) -> Self {
530        Self::new(v, v, v, v)
531    }
532
533    /// Create margin with `x` on left/right and `y` on top/bottom.
534    pub const fn xy(x: u32, y: u32) -> Self {
535        Self::new(y, x, y, x)
536    }
537
538    /// Create margin with explicit values for each side.
539    pub const fn new(top: u32, right: u32, bottom: u32, left: u32) -> Self {
540        Self {
541            top,
542            right,
543            bottom,
544            left,
545        }
546    }
547
548    /// Total horizontal margin (`left + right`).
549    pub const fn horizontal(self) -> u32 {
550        self.left + self.right
551    }
552
553    /// Total vertical margin (`top + bottom`).
554    pub const fn vertical(self) -> u32 {
555        self.top + self.bottom
556    }
557}
558
559/// Size constraints for layout computation.
560///
561/// All fields are optional. Unset constraints are unconstrained. Use the
562/// builder methods to set individual bounds in a fluent style.
563///
564/// # Example
565///
566/// ```
567/// use slt::Constraints;
568///
569/// let c = Constraints::default().min_w(10).max_w(40);
570/// ```
571#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
572#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
573#[must_use = "configure constraints using the returned value"]
574pub struct Constraints {
575    /// Minimum width in terminal columns, if any.
576    pub min_width: Option<u32>,
577    /// Maximum width in terminal columns, if any.
578    pub max_width: Option<u32>,
579    /// Minimum height in terminal rows, if any.
580    pub min_height: Option<u32>,
581    /// Maximum height in terminal rows, if any.
582    pub max_height: Option<u32>,
583}
584
585impl Constraints {
586    /// Set the minimum width constraint.
587    pub const fn min_w(mut self, min_width: u32) -> Self {
588        self.min_width = Some(min_width);
589        self
590    }
591
592    /// Set the maximum width constraint.
593    pub const fn max_w(mut self, max_width: u32) -> Self {
594        self.max_width = Some(max_width);
595        self
596    }
597
598    /// Set the minimum height constraint.
599    pub const fn min_h(mut self, min_height: u32) -> Self {
600        self.min_height = Some(min_height);
601        self
602    }
603
604    /// Set the maximum height constraint.
605    pub const fn max_h(mut self, max_height: u32) -> Self {
606        self.max_height = Some(max_height);
607        self
608    }
609}
610
611/// Cross-axis alignment within a container.
612///
613/// Controls how children are positioned along the axis perpendicular to the
614/// container's main axis. For a `row()`, this is vertical alignment; for a
615/// `col()`, this is horizontal alignment.
616#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
617#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
618pub enum Align {
619    /// Align children to the start of the cross axis (default).
620    #[default]
621    Start,
622    /// Center children on the cross axis.
623    Center,
624    /// Align children to the end of the cross axis.
625    End,
626}
627
628/// Main-axis content distribution within a container.
629///
630/// Controls how children are distributed along the main axis. For a `row()`,
631/// this is horizontal distribution; for a `col()`, this is vertical.
632///
633/// When children have `grow > 0`, they consume remaining space before justify
634/// distribution applies. Justify modes only affect the leftover space after
635/// flex-grow allocation.
636#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
637#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
638pub enum Justify {
639    /// Pack children at the start (default). Uses `gap` for spacing.
640    #[default]
641    Start,
642    /// Center children along the main axis with `gap` spacing.
643    Center,
644    /// Pack children at the end with `gap` spacing.
645    End,
646    /// First child at start, last at end, equal space between.
647    SpaceBetween,
648    /// Equal space around each child (half-size space at edges).
649    SpaceAround,
650    /// Equal space between all children and at both edges.
651    SpaceEvenly,
652}
653
654/// Text modifier bitflags stored as a `u8`.
655///
656/// Combine modifiers with `|` or [`Modifiers::insert`]. Check membership with
657/// [`Modifiers::contains`].
658#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
659#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
660#[cfg_attr(feature = "serde", serde(transparent))]
661pub struct Modifiers(pub u8);
662
663impl Modifiers {
664    /// No modifiers set.
665    pub const NONE: Self = Self(0);
666    /// Bold text.
667    pub const BOLD: Self = Self(1 << 0);
668    /// Dimmed/faint text.
669    pub const DIM: Self = Self(1 << 1);
670    /// Italic text.
671    pub const ITALIC: Self = Self(1 << 2);
672    /// Underlined text.
673    pub const UNDERLINE: Self = Self(1 << 3);
674    /// Reversed foreground/background colors.
675    pub const REVERSED: Self = Self(1 << 4);
676    /// Strikethrough text.
677    pub const STRIKETHROUGH: Self = Self(1 << 5);
678
679    /// Returns `true` if all bits in `other` are set in `self`.
680    #[inline]
681    pub fn contains(self, other: Self) -> bool {
682        (self.0 & other.0) == other.0
683    }
684
685    /// Set all bits from `other` into `self`.
686    #[inline]
687    pub fn insert(&mut self, other: Self) {
688        self.0 |= other.0;
689    }
690
691    /// Returns `true` if no modifiers are set.
692    #[inline]
693    pub fn is_empty(self) -> bool {
694        self.0 == 0
695    }
696}
697
698impl std::ops::BitOr for Modifiers {
699    type Output = Self;
700    #[inline]
701    fn bitor(self, rhs: Self) -> Self {
702        Self(self.0 | rhs.0)
703    }
704}
705
706impl std::ops::BitOrAssign for Modifiers {
707    #[inline]
708    fn bitor_assign(&mut self, rhs: Self) {
709        self.0 |= rhs.0;
710    }
711}
712
713/// Visual style for a terminal cell (foreground, background, modifiers).
714///
715/// Styles are applied to text via the builder methods on `Context` widget
716/// calls (e.g., `.bold()`, `.fg(Color::Cyan)`). All fields are optional;
717/// `None` means "inherit from the terminal default."
718///
719/// # Example
720///
721/// ```
722/// use slt::{Style, Color};
723///
724/// let style = Style::new().fg(Color::Cyan).bold();
725/// ```
726#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
727#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
728#[must_use = "build and pass the returned Style value"]
729pub struct Style {
730    /// Foreground color, or `None` to use the terminal default.
731    pub fg: Option<Color>,
732    /// Background color, or `None` to use the terminal default.
733    pub bg: Option<Color>,
734    /// Text modifiers (bold, italic, underline, etc.).
735    pub modifiers: Modifiers,
736}
737
738impl Style {
739    /// Create a new style with no color or modifiers set.
740    pub const fn new() -> Self {
741        Self {
742            fg: None,
743            bg: None,
744            modifiers: Modifiers::NONE,
745        }
746    }
747
748    /// Set the foreground color.
749    pub const fn fg(mut self, color: Color) -> Self {
750        self.fg = Some(color);
751        self
752    }
753
754    /// Set the background color.
755    pub const fn bg(mut self, color: Color) -> Self {
756        self.bg = Some(color);
757        self
758    }
759
760    /// Add the bold modifier.
761    pub fn bold(mut self) -> Self {
762        self.modifiers |= Modifiers::BOLD;
763        self
764    }
765
766    /// Add the dim modifier.
767    pub fn dim(mut self) -> Self {
768        self.modifiers |= Modifiers::DIM;
769        self
770    }
771
772    /// Add the italic modifier.
773    pub fn italic(mut self) -> Self {
774        self.modifiers |= Modifiers::ITALIC;
775        self
776    }
777
778    /// Add the underline modifier.
779    pub fn underline(mut self) -> Self {
780        self.modifiers |= Modifiers::UNDERLINE;
781        self
782    }
783
784    /// Add the reversed (inverted colors) modifier.
785    pub fn reversed(mut self) -> Self {
786        self.modifiers |= Modifiers::REVERSED;
787        self
788    }
789
790    /// Add the strikethrough modifier.
791    pub fn strikethrough(mut self) -> Self {
792        self.modifiers |= Modifiers::STRIKETHROUGH;
793        self
794    }
795}