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}