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}