Skip to main content

retroglyph_core/
style.rs

1//! Text styling: foreground and background color.
2
3use crate::color::Color;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
6/// A style consisting of foreground and background color.
7///
8/// No text modifiers (bold, italic, underline, etc.) by design: retroglyph is a spiritual
9/// remake of `BearLibTerminal`, which doesn't support them either. A pixel/bitmap-font renderer
10/// can't fake most of them (no bold font variant, no underline stroke) without real per-style
11/// assets, so rather than have them work in a real terminal and silently do nothing in the
12/// software backend, they're not part of the API at all. Color and glyph choice are the only two
13/// knobs, in every backend.
14///
15/// # Examples
16///
17/// ```
18/// use retroglyph_core::{Color, Style};
19///
20/// let style = Style::new().fg(Color::GREEN).bg(Color::BLACK);
21/// assert_eq!(style.foreground(), Color::GREEN);
22/// assert_eq!(style.background(), Color::BLACK);
23/// ```
24pub struct Style {
25    /// Foreground color.
26    ///
27    /// Colors the cell's glyph. A cell that a pixel backend draws as a *sprite* is the one
28    /// exception: a sprite is composited from its own pixels and `fg` does not tint it. See
29    /// [`Surface::put_span`](crate::Surface::put_span).
30    pub(crate) fg: Color,
31    /// Background color.
32    ///
33    /// Fills the cell behind the glyph. Behind a sprite it is still painted, so it shows through
34    /// the sprite's transparent pixels.
35    pub(crate) bg: Color,
36}
37
38impl Style {
39    /// Creates a new style with default values.
40    #[must_use]
41    pub fn new() -> Self {
42        Self::default()
43    }
44
45    /// Sets the foreground color, which colors the cell's glyph.
46    ///
47    /// Does not tint a sprite: on a pixel backend, a cell whose glyph resolves to a sprite is
48    /// composited from the sprite's own pixels and ignores this color entirely. The same cell
49    /// drawn by a cell backend falls back to its glyph and *is* colored by it, so one value can
50    /// read very differently across backends. See
51    /// [`Surface::put_span`](crate::Surface::put_span).
52    #[must_use]
53    pub const fn fg(mut self, color: Color) -> Self {
54        self.fg = color;
55        self
56    }
57
58    /// Sets the background color.
59    #[must_use]
60    pub const fn bg(mut self, color: Color) -> Self {
61        self.bg = color;
62        self
63    }
64
65    /// Returns the foreground color.
66    #[must_use]
67    pub const fn foreground(&self) -> Color {
68        self.fg
69    }
70
71    /// Returns the background color.
72    #[must_use]
73    pub const fn background(&self) -> Color {
74        self.bg
75    }
76
77    /// Overlays another style onto this one, only if fields in `other` are non-default.
78    ///
79    /// `Color::Default` in `other` means "unset", not "reset to default": a field left at
80    /// `Color::Default` is skipped, and `self`'s existing value for that field is kept. This
81    /// mirrors ratatui's `Style::patch` convention, so `Style::new().fg(Color::Default)` is a
82    /// no-op when patched onto anything, and there is no way to use `patch` to explicitly clear a
83    /// field back to `Color::Default`; use [`Style::reset_fg`] or [`Style::reset_bg`] for that.
84    ///
85    /// ```
86    /// use retroglyph_core::{Color, Style};
87    ///
88    /// let base = Style::new().fg(Color::RED).bg(Color::BLUE);
89    ///
90    /// // Patching with a default `fg` leaves `base`'s red foreground untouched.
91    /// let patched = base.patch(Style::new().bg(Color::GREEN));
92    /// assert_eq!(patched.foreground(), Color::RED);
93    /// assert_eq!(patched.background(), Color::GREEN);
94    /// ```
95    #[must_use]
96    pub fn patch(mut self, other: Self) -> Self {
97        if other.fg != Color::Default {
98            self.fg = other.fg;
99        }
100        if other.bg != Color::Default {
101            self.bg = other.bg;
102        }
103        self
104    }
105
106    /// Resets the foreground color to `Color::Default`.
107    ///
108    /// Unlike [`Style::patch`], which treats `Color::Default` as "leave unset", this explicitly
109    /// clears the field. Use this when a caller needs to undo a previously patched-in foreground
110    /// color rather than merge in a new one.
111    #[must_use]
112    pub const fn reset_fg(mut self) -> Self {
113        self.fg = Color::Default;
114        self
115    }
116
117    /// Resets the background color to `Color::Default`.
118    ///
119    /// Unlike [`Style::patch`], which treats `Color::Default` as "leave unset", this explicitly
120    /// clears the field. Use this when a caller needs to undo a previously patched-in background
121    /// color rather than merge in a new one.
122    #[must_use]
123    pub const fn reset_bg(mut self) -> Self {
124        self.bg = Color::Default;
125        self
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    #[test]
134    fn test_style_builder() {
135        let s = Style::new().fg(Color::RED).bg(Color::BLUE);
136        assert_eq!(s.foreground(), Color::RED);
137        assert_eq!(s.background(), Color::BLUE);
138    }
139
140    #[test]
141    fn test_patch_keeps_non_default_fields() {
142        let base = Style::new().fg(Color::RED).bg(Color::BLUE);
143        let patched = base.patch(Style::new().fg(Color::GREEN));
144        assert_eq!(patched.foreground(), Color::GREEN);
145        assert_eq!(patched.background(), Color::BLUE);
146    }
147
148    #[test]
149    fn test_patch_cannot_reset_a_field_to_default() {
150        let base = Style::new().fg(Color::RED).bg(Color::BLUE);
151        let patched = base.patch(Style::new());
152        assert_eq!(patched.foreground(), Color::RED);
153        assert_eq!(patched.background(), Color::BLUE);
154    }
155
156    #[test]
157    fn test_reset_fg_and_reset_bg_clear_to_default() {
158        let s = Style::new().fg(Color::RED).bg(Color::BLUE);
159        assert_eq!(s.reset_fg().foreground(), Color::Default);
160        assert_eq!(s.reset_bg().background(), Color::Default);
161    }
162}