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.
14pub struct Style {
15    /// Foreground color.
16    pub(crate) fg: Color,
17    /// Background color.
18    pub(crate) bg: Color,
19}
20
21impl Style {
22    /// Creates a new style with default values.
23    #[must_use]
24    pub fn new() -> Self {
25        Self::default()
26    }
27
28    /// Sets the foreground color.
29    #[must_use]
30    pub const fn fg(mut self, color: Color) -> Self {
31        self.fg = color;
32        self
33    }
34
35    /// Sets the background color.
36    #[must_use]
37    pub const fn bg(mut self, color: Color) -> Self {
38        self.bg = color;
39        self
40    }
41
42    /// Returns the foreground color.
43    #[must_use]
44    pub const fn foreground(&self) -> Color {
45        self.fg
46    }
47
48    /// Returns the background color.
49    #[must_use]
50    pub const fn background(&self) -> Color {
51        self.bg
52    }
53
54    /// Overlays another style onto this one, only if fields in `other` are non-default.
55    ///
56    /// `Color::Default` in `other` means "unset", not "reset to default": a field left at
57    /// `Color::Default` is skipped, and `self`'s existing value for that field is kept. This
58    /// mirrors ratatui's `Style::patch` convention, so `Style::new().fg(Color::Default)` is a
59    /// no-op when patched onto anything, and there is no way to use `patch` to explicitly clear a
60    /// field back to `Color::Default` -- use [`Style::reset_fg`] or [`Style::reset_bg`] for that.
61    ///
62    /// ```
63    /// use retroglyph_core::{Color, Style};
64    ///
65    /// let base = Style::new().fg(Color::RED).bg(Color::BLUE);
66    ///
67    /// // Patching with a default `fg` leaves `base`'s red foreground untouched.
68    /// let patched = base.patch(Style::new().bg(Color::GREEN));
69    /// assert_eq!(patched.foreground(), Color::RED);
70    /// assert_eq!(patched.background(), Color::GREEN);
71    /// ```
72    #[must_use]
73    pub fn patch(mut self, other: Self) -> Self {
74        if other.fg != Color::Default {
75            self.fg = other.fg;
76        }
77        if other.bg != Color::Default {
78            self.bg = other.bg;
79        }
80        self
81    }
82
83    /// Resets the foreground color to `Color::Default`.
84    ///
85    /// Unlike [`Style::patch`], which treats `Color::Default` as "leave unset", this explicitly
86    /// clears the field. Use this when a caller needs to undo a previously patched-in foreground
87    /// color rather than merge in a new one.
88    #[must_use]
89    pub const fn reset_fg(mut self) -> Self {
90        self.fg = Color::Default;
91        self
92    }
93
94    /// Resets the background color to `Color::Default`.
95    ///
96    /// Unlike [`Style::patch`], which treats `Color::Default` as "leave unset", this explicitly
97    /// clears the field. Use this when a caller needs to undo a previously patched-in background
98    /// color rather than merge in a new one.
99    #[must_use]
100    pub const fn reset_bg(mut self) -> Self {
101        self.bg = Color::Default;
102        self
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    #[test]
111    fn test_style_builder() {
112        let s = Style::new().fg(Color::RED).bg(Color::BLUE);
113        assert_eq!(s.foreground(), Color::RED);
114        assert_eq!(s.background(), Color::BLUE);
115    }
116
117    #[test]
118    fn test_patch_keeps_non_default_fields() {
119        let base = Style::new().fg(Color::RED).bg(Color::BLUE);
120        let patched = base.patch(Style::new().fg(Color::GREEN));
121        assert_eq!(patched.foreground(), Color::GREEN);
122        assert_eq!(patched.background(), Color::BLUE);
123    }
124
125    #[test]
126    fn test_patch_cannot_reset_a_field_to_default() {
127        let base = Style::new().fg(Color::RED).bg(Color::BLUE);
128        let patched = base.patch(Style::new());
129        assert_eq!(patched.foreground(), Color::RED);
130        assert_eq!(patched.background(), Color::BLUE);
131    }
132
133    #[test]
134    fn test_reset_fg_and_reset_bg_clear_to_default() {
135        let s = Style::new().fg(Color::RED).bg(Color::BLUE);
136        assert_eq!(s.reset_fg().foreground(), Color::Default);
137        assert_eq!(s.reset_bg().background(), Color::Default);
138    }
139}