Skip to main content

retroglyph/
style.rs

1//! Text styling and modifiers.
2
3use crate::color::Color;
4
5bitflags::bitflags! {
6    /// Text attributes applied to a cell (bold, italic, etc.).
7    ///
8    /// Combine with `|` and test with `contains`.
9    ///
10    /// # Examples
11    ///
12    /// ```
13    /// use retroglyph::style::CellModifier;
14    ///
15    /// let attrs = CellModifier::BOLD | CellModifier::ITALIC;
16    /// assert!(attrs.contains(CellModifier::BOLD));
17    /// assert!(attrs.contains(CellModifier::ITALIC));
18    /// assert!(!attrs.contains(CellModifier::UNDERLINE));
19    /// ```
20    #[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
21    pub struct CellModifier: u8 {
22        /// Bold text.
23        const BOLD = 1 << 0;
24        /// Dim text.
25        const DIM = 1 << 1;
26        /// Italic text.
27        const ITALIC = 1 << 2;
28        /// Underlined text.
29        const UNDERLINE = 1 << 3;
30        /// Blinking text.
31        const BLINK = 1 << 4;
32        /// Reversed colors.
33        const REVERSE = 1 << 5;
34        /// Hidden text.
35        const HIDDEN = 1 << 6;
36        /// Strikethrough text.
37        const STRIKETHROUGH = 1 << 7;
38    }
39}
40
41impl core::fmt::Debug for CellModifier {
42    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
43        // bitflags 2.x doesn't auto-generate Debug. We want the
44        // human-readable "BOLD | ITALIC" / "NONE" format.
45        if self.is_empty() {
46            return f.write_str("NONE");
47        }
48        let mut sep = "";
49        if self.contains(Self::BOLD) {
50            write!(f, "{sep}BOLD")?;
51            sep = " | ";
52        }
53        if self.contains(Self::DIM) {
54            write!(f, "{sep}DIM")?;
55            sep = " | ";
56        }
57        if self.contains(Self::ITALIC) {
58            write!(f, "{sep}ITALIC")?;
59            sep = " | ";
60        }
61        if self.contains(Self::UNDERLINE) {
62            write!(f, "{sep}UNDERLINE")?;
63            sep = " | ";
64        }
65        if self.contains(Self::BLINK) {
66            write!(f, "{sep}BLINK")?;
67            sep = " | ";
68        }
69        if self.contains(Self::REVERSE) {
70            write!(f, "{sep}REVERSE")?;
71            sep = " | ";
72        }
73        if self.contains(Self::HIDDEN) {
74            write!(f, "{sep}HIDDEN")?;
75            sep = " | ";
76        }
77        if self.contains(Self::STRIKETHROUGH) {
78            write!(f, "{sep}STRIKETHROUGH")?;
79        }
80        Ok(())
81    }
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
85/// A style consisting of foreground, background, and text modifiers.
86pub struct Style {
87    /// Foreground color.
88    pub(crate) fg: Color,
89    /// Background color.
90    pub(crate) bg: Color,
91    /// Text modifiers.
92    pub(crate) modifiers: CellModifier,
93}
94
95impl Style {
96    /// Creates a new style with default values.
97    #[must_use]
98    pub fn new() -> Self {
99        Self::default()
100    }
101
102    /// Sets the foreground color.
103    #[must_use]
104    pub const fn fg(mut self, color: Color) -> Self {
105        self.fg = color;
106        self
107    }
108
109    /// Sets the background color.
110    #[must_use]
111    pub const fn bg(mut self, color: Color) -> Self {
112        self.bg = color;
113        self
114    }
115
116    /// Adds the bold modifier.
117    #[must_use]
118    pub fn bold(mut self) -> Self {
119        self.modifiers |= CellModifier::BOLD;
120        self
121    }
122
123    /// Adds the italic modifier.
124    #[must_use]
125    pub fn italic(mut self) -> Self {
126        self.modifiers |= CellModifier::ITALIC;
127        self
128    }
129
130    /// Returns the foreground color.
131    #[must_use]
132    pub const fn foreground(&self) -> Color {
133        self.fg
134    }
135
136    /// Returns the background color.
137    #[must_use]
138    pub const fn background(&self) -> Color {
139        self.bg
140    }
141
142    /// Returns the text modifiers.
143    #[must_use]
144    pub const fn modifiers(&self) -> CellModifier {
145        self.modifiers
146    }
147
148    /// Overlays another style onto this one, only if fields in `other` are non-default.
149    #[must_use]
150    pub fn patch(mut self, other: Self) -> Self {
151        if other.fg != Color::Default {
152            self.fg = other.fg;
153        }
154        if other.bg != Color::Default {
155            self.bg = other.bg;
156        }
157        self.modifiers |= other.modifiers;
158        self
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165
166    #[test]
167    fn test_modifier_ops() {
168        let m = CellModifier::BOLD | CellModifier::ITALIC;
169        assert!(m.contains(CellModifier::BOLD));
170        assert!(m.contains(CellModifier::ITALIC));
171        assert!(!m.contains(CellModifier::UNDERLINE));
172    }
173
174    #[test]
175    fn test_modifier_debug() {
176        assert_eq!(format!("{:?}", CellModifier::empty()), "NONE");
177        assert_eq!(
178            format!("{:?}", CellModifier::BOLD | CellModifier::ITALIC),
179            "BOLD | ITALIC"
180        );
181        assert_eq!(
182            format!("{:?}", CellModifier::STRIKETHROUGH),
183            "STRIKETHROUGH"
184        );
185    }
186
187    #[test]
188    fn test_style_builder() {
189        let s = Style::new().fg(Color::RED).bold();
190        assert_eq!(s.foreground(), Color::RED);
191        assert!(s.modifiers().contains(CellModifier::BOLD));
192    }
193}