1use ctl_text::Style;
9use ctl_text::anstyle::{AnsiColor, Color};
10
11use crate::document::{NoticeLevel, Role};
12
13const fn bold(color: AnsiColor) -> Style {
15 Style::new().bold().fg_color(Some(Color::Ansi(color)))
16}
17
18#[cfg_attr(
66 feature = "view",
67 doc = "",
68 doc = "[`View::theme`]: crate::View::theme"
69)]
70#[cfg_attr(
71 not(feature = "view"),
72 doc = "",
73 doc = "[`View::theme`]: crate#cargo-features"
74)]
75#[cfg_attr(feature = "app", doc = "", doc = "[`App::theme`]: crate::App::theme")]
76#[cfg_attr(
77 not(feature = "app"),
78 doc = "",
79 doc = "[`App::theme`]: crate#cargo-features"
80)]
81#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
82pub struct Theme {
83 plain: Style,
84 heading: Style,
85 success: Style,
86 warning: Style,
87 error: Style,
88 value: Style,
89 muted: Style,
90 token: Style,
91 id: Style,
92 success_notice: Option<Style>,
94 warning_notice: Option<Style>,
95 error_notice: Option<Style>,
96}
97
98impl Theme {
99 #[must_use]
103 pub const fn new() -> Self {
104 Self {
105 plain: Style::new(),
106 heading: bold(AnsiColor::Cyan),
107 success: bold(AnsiColor::Green),
108 warning: bold(AnsiColor::Yellow),
109 error: bold(AnsiColor::Red),
110 value: bold(AnsiColor::Yellow),
111 muted: Style::new().fg_color(Some(Color::Ansi(AnsiColor::BrightBlack))),
112 token: bold(AnsiColor::Green),
113 id: Style::new().bold(),
114 success_notice: None,
115 warning_notice: None,
116 error_notice: None,
117 }
118 }
119
120 #[must_use]
122 pub const fn role_style(self, role: Role) -> Style {
123 match role {
124 Role::Plain => self.plain,
125 Role::Heading => self.heading,
126 Role::Success => self.success,
127 Role::Warning => self.warning,
128 Role::Error => self.error,
129 Role::Value => self.value,
130 Role::Muted => self.muted,
131 Role::Token => self.token,
132 Role::Id => self.id,
133 }
134 }
135
136 #[must_use]
138 pub const fn style(mut self, role: Role, style: Style) -> Self {
139 match role {
140 Role::Plain => self.plain = style,
141 Role::Heading => self.heading = style,
142 Role::Success => self.success = style,
143 Role::Warning => self.warning = style,
144 Role::Error => self.error = style,
145 Role::Value => self.value = style,
146 Role::Muted => self.muted = style,
147 Role::Token => self.token = style,
148 Role::Id => self.id = style,
149 }
150 self
151 }
152
153 #[must_use]
157 pub const fn label_style(self, level: NoticeLevel) -> Style {
158 let (own, role) = match level {
159 NoticeLevel::Success => (self.success_notice, self.success),
160 NoticeLevel::Warning => (self.warning_notice, self.warning),
161 NoticeLevel::Error => (self.error_notice, self.error),
162 };
163 match own {
164 Some(style) => style,
165 None => role,
166 }
167 }
168
169 #[must_use]
171 pub const fn notice_style(mut self, level: NoticeLevel, style: Style) -> Self {
172 match level {
173 NoticeLevel::Success => self.success_notice = Some(style),
174 NoticeLevel::Warning => self.warning_notice = Some(style),
175 NoticeLevel::Error => self.error_notice = Some(style),
176 }
177 self
178 }
179}
180
181impl Default for Theme {
182 fn default() -> Self {
184 Self::new()
185 }
186}
187
188#[cfg(test)]
189mod tests {
190 use ctl_text::Style;
191 use ctl_text::anstyle::{AnsiColor, Color, Effects};
192
193 use super::Theme;
194 use crate::document::{NoticeLevel, Role};
195
196 #[test]
197 fn the_default_palette_is_bold_named_colours() {
198 let theme = Theme::default();
199 let cyan = Style::new()
200 .bold()
201 .fg_color(Some(Color::Ansi(AnsiColor::Cyan)));
202 assert_eq!(theme.role_style(Role::Heading), cyan);
203 assert_eq!(theme.role_style(Role::Plain), Style::new());
204 assert_eq!(theme.role_style(Role::Id), Style::new().bold());
205 assert_eq!(
206 theme.label_style(NoticeLevel::Error),
207 theme.role_style(Role::Error)
208 );
209 }
210
211 #[test]
212 fn replacing_one_role_keeps_the_others() {
213 let underline = Style::new().effects(Effects::UNDERLINE);
214 let theme = Theme::new().style(Role::Muted, underline);
215 assert_eq!(theme.role_style(Role::Muted), underline);
216 assert_eq!(
217 theme.role_style(Role::Value),
218 Theme::new().role_style(Role::Value)
219 );
220 assert_eq!(
221 theme.label_style(NoticeLevel::Warning),
222 Theme::new().label_style(NoticeLevel::Warning)
223 );
224 }
225
226 #[test]
227 fn a_notice_label_has_its_own_style() {
228 let plain = Theme::new().notice_style(NoticeLevel::Success, Style::new());
229 assert_eq!(plain.label_style(NoticeLevel::Success), Style::new());
230 assert_eq!(
231 plain.role_style(Role::Success),
232 Theme::new().role_style(Role::Success)
233 );
234 }
235
236 #[test]
237 fn a_notice_label_follows_its_role_until_given_its_own_style() {
238 let magenta = Style::new().fg_color(Some(Color::Ansi(AnsiColor::Magenta)));
239 let follows = Theme::new().style(Role::Error, magenta);
240 assert_eq!(follows.label_style(NoticeLevel::Error), magenta);
241
242 let underline = Style::new().effects(Effects::UNDERLINE);
243 let own = Theme::new()
244 .notice_style(NoticeLevel::Error, underline)
245 .style(Role::Error, magenta);
246 assert_eq!(own.label_style(NoticeLevel::Error), underline);
247 assert_eq!(own.role_style(Role::Error), magenta);
248 }
249}