1use std::collections::HashMap;
4
5use ratatui_core::buffer::{Buffer, Cell};
6use ratatui_core::style::{Color, Modifier};
7
8use crate::color::{ColorDepth, Rgb};
9use crate::geometry::Padding;
10use crate::theme::{Paint, PropValue, StyleProps};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
14pub struct CellStyle {
15 pub fg: Option<Rgb>,
17 pub bg: Option<Rgb>,
19 pub bold: bool,
21 pub italic: bool,
23 pub underline: bool,
25 pub dim: bool,
27}
28
29impl CellStyle {
30 #[must_use]
32 pub fn fg(color: Rgb) -> Self {
33 Self { fg: Some(color), ..Self::default() }
34 }
35
36 #[must_use]
38 pub fn on(mut self, color: Rgb) -> Self {
39 self.bg = Some(color);
40 self
41 }
42
43 #[must_use]
45 pub fn with_bold(mut self, bold: bool) -> Self {
46 self.bold = bold;
47 self
48 }
49
50 #[cfg(test)]
52 pub(crate) fn apply(self, cell: &mut Cell) {
53 self.paint().apply(cell);
54 }
55
56 pub(crate) fn paint(self) -> CellPaint {
58 let mut modifier = Modifier::empty();
59 modifier.set(Modifier::BOLD, self.bold);
60 modifier.set(Modifier::ITALIC, self.italic);
61 modifier.set(Modifier::UNDERLINED, self.underline);
62 modifier.set(Modifier::DIM, self.dim);
63 CellPaint { fg: self.fg.map(to_color), bg: self.bg.map(to_color), modifier }
64 }
65}
66
67#[derive(Debug, Clone, Copy)]
70pub(crate) struct CellPaint {
71 fg: Option<Color>,
72 bg: Option<Color>,
73 modifier: Modifier,
74}
75
76impl CellPaint {
77 pub(crate) fn apply(self, cell: &mut Cell) {
79 if let Some(fg) = self.fg {
80 cell.fg = fg;
81 }
82 if let Some(bg) = self.bg {
83 cell.bg = bg;
84 }
85 cell.modifier = self.modifier;
86 }
87}
88
89pub(crate) fn to_color(color: Rgb) -> Color {
96 Color::Rgb(color.r, color.g, color.b)
97}
98
99pub(crate) fn reduce(buf: &mut Buffer, depth: ColorDepth, ground: Rgb) {
110 match depth {
111 ColorDepth::TrueColor => {}
112 ColorDepth::Ansi256 => reduce_with(buf, Rgb::to_ansi256, Rgb::to_ansi256_text, true),
113 ColorDepth::Ansi16 => {
114 let text = |text: Rgb, bg| text.to_ansi16_text(bg, ground);
115 reduce_with(buf, |tone| tone.to_ansi16_on(ground), text, false);
116 }
117 }
118}
119
120fn is_half_block(symbol: &str) -> bool {
122 matches!(symbol, "▀" | "▄")
123}
124
125fn reduce_with(buf: &mut Buffer, tone: impl Fn(Rgb) -> u8, text: impl Fn(Rgb, Rgb) -> u8, half_blocks_fill: bool) {
129 let mut tones: HashMap<Rgb, u8> = HashMap::new();
132 let mut texts: HashMap<(Rgb, Rgb), u8> = HashMap::new();
133 let mut last: Option<(CellColours, (Color, Color))> = None;
134 for cell in &mut buf.content {
135 let (bg, fg) = (rgb(cell.bg), rgb(cell.fg));
136 if bg.is_none() && fg.is_none() {
137 continue;
138 }
139 let symbol = cell.symbol();
140 let glyph =
141 fg.is_some() && bg.is_some() && !symbol.trim().is_empty() && !(half_blocks_fill && is_half_block(symbol));
142 let key = (cell.fg, cell.bg, glyph);
143 if let Some((seen, (fg, bg))) = last
144 && seen == key
145 {
146 (cell.fg, cell.bg) = (fg, bg);
147 continue;
148 }
149 if let Some(fg) = fg {
150 let index = match bg {
151 Some(bg) if glyph => *texts.entry((fg, bg)).or_insert_with(|| text(fg, bg)),
152 _ => *tones.entry(fg).or_insert_with(|| tone(fg)),
153 };
154 cell.fg = Color::Indexed(index);
155 }
156 if let Some(bg) = bg {
157 cell.bg = Color::Indexed(*tones.entry(bg).or_insert_with(|| tone(bg)));
158 }
159 last = Some((key, (cell.fg, cell.bg)));
160 }
161}
162
163type CellColours = (Color, Color, bool);
165
166fn rgb(color: Color) -> Option<Rgb> {
168 match color {
169 Color::Rgb(r, g, b) => Some(Rgb::new(r, g, b)),
170 _ => None,
171 }
172}
173
174#[derive(Debug, Clone, PartialEq)]
176pub struct WidgetStyle {
177 props: StyleProps,
178 phase: f32,
179}
180
181impl WidgetStyle {
182 pub(crate) fn new(props: StyleProps, phase: f32) -> Self {
183 Self { props, phase }
184 }
185
186 #[must_use]
189 pub(crate) fn without(mut self, key: &str) -> Self {
190 self.props.remove(key);
191 self
192 }
193
194 #[must_use]
196 pub fn color(&self, key: &str) -> Option<Rgb> {
197 self.props.paint(key).map(|paint: Paint| paint.at(self.phase))
198 }
199
200 #[must_use]
202 pub fn flag(&self, key: &str) -> bool {
203 self.props.flag(key)
204 }
205
206 #[must_use]
208 pub fn cells(&self, key: &str) -> Option<u16> {
209 self.props.cells(key)
210 }
211
212 #[must_use]
214 pub fn word(&self, key: &str) -> Option<&'static str> {
215 self.props.word(key)
216 }
217
218 #[must_use]
220 pub fn padding(&self) -> Padding {
221 match self.props.get("padding") {
222 Some(PropValue::Pair(v, h)) => Padding::symmetric(v, h),
223 Some(PropValue::Cells(n)) => Padding::all(n),
224 _ => Padding::default(),
225 }
226 }
227
228 #[must_use]
230 pub fn text(&self) -> CellStyle {
231 CellStyle {
232 fg: self.color("fg"),
233 bg: self.color("bg"),
234 bold: self.flag("bold"),
235 italic: self.flag("italic"),
236 underline: self.flag("underline"),
237 dim: self.flag("dim"),
238 }
239 }
240
241 #[must_use]
243 pub fn is_animated(&self) -> bool {
244 self.props.is_animated()
245 }
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251 use crate::theme::ThemeRegistry;
252
253 #[test]
254 fn applies_colours_and_modifiers() {
255 let mut cell = Cell::default();
256 CellStyle::fg(Rgb::new(255, 0, 0)).on(Rgb::new(0, 0, 0)).with_bold(true).apply(&mut cell);
257 assert_eq!(cell.fg, Color::Rgb(255, 0, 0));
258 assert!(cell.modifier.contains(Modifier::BOLD));
259 CellStyle::fg(Rgb::new(0, 0, 255)).apply(&mut cell);
260 assert_eq!(cell.fg, Color::Rgb(0, 0, 255));
261 assert_eq!(cell.bg, Color::Rgb(0, 0, 0), "a style without a background keeps the one there");
262 assert!(!cell.modifier.contains(Modifier::BOLD));
263 }
264
265 #[test]
266 fn a_frame_is_reduced_to_its_palette_once_painted() {
267 let ground = Rgb::new(12, 12, 14);
268 let painted = || {
269 let mut buf = Buffer::empty(ratatui_core::layout::Rect::new(0, 0, 3, 1));
270 CellStyle::fg(Rgb::new(255, 0, 0)).on(Rgb::new(0, 0, 0)).apply(&mut buf.content[0]);
271 buf.content[0].set_symbol("a");
272 buf.content[1].set_bg(Color::Indexed(4));
273 buf.content[2].set_bg(Color::Rgb(128, 128, 128));
274 buf
275 };
276 let mut buf = painted();
277 reduce(&mut buf, ColorDepth::TrueColor, ground);
278 assert_eq!(buf, painted(), "true colour is sent as painted");
279 reduce(&mut buf, ColorDepth::Ansi256, ground);
280 assert_eq!((buf.content[0].fg, buf.content[0].bg), (Color::Indexed(196), Color::Indexed(16)));
281 assert_eq!(buf.content[1].bg, Color::Indexed(4), "a palette colour is left as it is");
282 assert_eq!(buf.content[2].bg, Color::Indexed(244));
283 let mut buf = painted();
284 reduce(&mut buf, ColorDepth::Ansi16, ground);
285 assert_eq!((buf.content[0].fg, buf.content[0].bg), (Color::Indexed(9), Color::Indexed(0)));
286 assert_eq!(buf.content[2].bg, Color::Indexed(8));
287 }
288
289 #[test]
290 fn a_half_block_in_256_colours_is_two_fills_not_text() {
291 let (top, bottom) = (Rgb::new(120, 120, 120), Rgb::new(124, 124, 124));
292 let painted = |symbol: &str| {
293 let mut buf = Buffer::empty(ratatui_core::layout::Rect::new(0, 0, 1, 1));
294 CellStyle::fg(top).on(bottom).apply(&mut buf.content[0]);
295 buf.content[0].set_symbol(symbol);
296 buf
297 };
298 let mut buf = painted("▀");
299 reduce(&mut buf, ColorDepth::Ansi256, Rgb::new(12, 12, 14));
300 assert_eq!(buf.content[0].fg, Color::Indexed(top.to_ansi256()), "the upper half is its nearest entry");
301 assert_eq!(buf.content[0].bg, Color::Indexed(bottom.to_ansi256()));
302 let mut buf = painted("a");
303 reduce(&mut buf, ColorDepth::Ansi256, Rgb::new(12, 12, 14));
304 assert_ne!(buf.content[0].fg, Color::Indexed(top.to_ansi256()), "a letter is still kept readable");
305 }
306
307 #[test]
308 fn resolves_theme_properties() {
309 let (theme, _) = ThemeRegistry::builtin().resolve_or_default("monochrome");
310 let style = WidgetStyle::new(theme.style("button", Some("primary"), &[]), 0.0);
311 assert_ne!(style.text().bg, theme.color("accent"));
313 assert_eq!(style.text().fg, theme.color("accent"));
314 assert!(style.text().bold);
315 assert_eq!(style.padding(), Padding::symmetric(0, 2));
316 }
317}