1use itertools::Itertools;
5use ratatui_core::buffer::Buffer;
6use ratatui_core::layout::Rect;
7use ratatui_core::style::Color;
8use ratatui_core::widgets::Widget; const RATATUI_MASCOT: &str = indoc::indoc! {"
11 hhh
12 hhhhhh
13 hhhhhhh
14 hhhhhhhh
15 hhhhhhhhh
16 hhhhhhhhhh
17 hhhhhhhhhhhh
18 hhhhhhhhhhhhh
19 hhhhhhhhhhhhh ██████
20 hhhhhhhhhhh ████████
21 hhhhh ███████████
22 hhh ██ee████████
23 h █████████████
24 ████ █████████████
25 █████████████████
26 ████████████████
27 ████████████████
28 ███ ██████████
29 ▒▒ █████████
30 ▒░░▒ █████████
31 ▒░░░░▒ ██████████
32 ▒░░▓░░░▒ █████████
33 ▒░░▓▓░░░░▒ ████████
34 ▒░░░░░░░░░░▒ ██████████
35 ▒░░░░░░░░░░░░▒ ██████████
36 ▒░░░░░░░▓▓░░░░░▒ █████████
37 ▒░░░░░░░░░▓▓░░░░░▒ ████ ███
38 ▒░░░░░░░░░░░░░░░░░░▒ ██ ███
39 ▒░░░░░░░░░░░░░░░░░░░░▒ █ ███
40 ▒░░░░░░░░░░░░░░░░░░░░░▒ ███
41 ▒░░░░░░░░░░░░░░░░░░░░░▒ ███
42 ▒░░░░░░░░░░░░░░░░░░░░░▒ █"
43};
44
45const EMPTY: char = ' ';
46const RAT: char = '█';
47const HAT: char = 'h';
48const EYE: char = 'e';
49const TERM: char = '░';
50const TERM_BORDER: char = '▒';
51const TERM_CURSOR: char = '▓';
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
55pub enum MascotEyeColor {
56 #[default]
58 Default,
59
60 Red,
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub struct RatatuiMascot {
67 eye_state: MascotEyeColor,
68 rat_color: Color,
70 rat_eye_color: Color,
72 rat_eye_blink: Color,
74 hat_color: Color,
76 term_color: Color,
78 term_border_color: Color,
80 term_cursor_color: Color,
82}
83
84impl Default for RatatuiMascot {
85 fn default() -> Self {
86 Self {
87 rat_color: Color::Indexed(252), hat_color: Color::Indexed(231), rat_eye_color: Color::Indexed(236), rat_eye_blink: Color::Indexed(196), term_color: Color::Indexed(232), term_border_color: Color::Indexed(237), term_cursor_color: Color::Indexed(248), eye_state: MascotEyeColor::Default,
95 }
96 }
97}
98
99impl RatatuiMascot {
100 pub fn new() -> Self {
102 Self {
103 ..Default::default()
104 }
105 }
106
107 #[must_use]
109 pub const fn set_eye(self, rat_eye: MascotEyeColor) -> Self {
110 Self {
111 eye_state: rat_eye,
112 ..self
113 }
114 }
115
116 const fn color_for(&self, c: char) -> Option<Color> {
117 match c {
118 RAT => Some(self.rat_color),
119 HAT => Some(self.hat_color),
120 EYE => Some(match self.eye_state {
121 MascotEyeColor::Default => self.rat_eye_color,
122 MascotEyeColor::Red => self.rat_eye_blink,
123 }),
124 TERM => Some(self.term_color),
125 TERM_CURSOR => Some(self.term_cursor_color),
126 TERM_BORDER => Some(self.term_border_color),
127 _ => None,
128 }
129 }
130}
131
132impl Widget for RatatuiMascot {
133 fn render(self, area: Rect, buf: &mut Buffer) {
138 let area = area.intersection(buf.area);
139 if area.is_empty() {
140 return;
141 }
142
143 for (y, (line1, line2)) in RATATUI_MASCOT.lines().tuples().enumerate() {
144 for (x, (ch1, ch2)) in line1.chars().zip(line2.chars()).enumerate() {
145 let x = area.left() + x as u16;
146 let y = area.top() + y as u16;
147
148 if x >= area.right() || y >= area.bottom() {
150 continue;
151 }
152
153 let cell = &mut buf[(x, y)];
154 let (fg, bg) = match (ch1, ch2) {
157 (EMPTY, EMPTY) => (None, None),
158 (c, EMPTY) | (EMPTY, c) => (self.color_for(c), None),
159 (TERM, TERM_BORDER) => (self.color_for(TERM_BORDER), self.color_for(TERM)),
160 (TERM, c) | (c, TERM) => (self.color_for(c), self.color_for(TERM)),
161 (c1, c2) => (self.color_for(c1), self.color_for(c2)),
162 };
163 let symbol = match (ch1, ch2) {
165 (EMPTY, EMPTY) => None,
166 (TERM, TERM) => Some(EMPTY),
167 (_, EMPTY | TERM) => Some('▀'),
168 (EMPTY | TERM, _) => Some('▄'),
169 (c, d) if c == d => Some('█'),
170 (_, _) => Some('▀'),
171 };
172 if let Some(fg) = fg {
173 cell.fg = fg;
174 }
175 if let Some(bg) = bg {
176 cell.bg = bg;
177 }
178 if let Some(symb) = symbol {
179 cell.set_char(symb);
180 }
181 }
182 }
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use alloc::string::String;
189
190 use super::*;
191
192 #[test]
193 fn new_mascot() {
194 let mascot = RatatuiMascot::new();
195 assert_eq!(mascot.eye_state, MascotEyeColor::Default);
196 }
197
198 #[test]
199 fn set_eye_color() {
200 let mut buf = Buffer::empty(Rect::new(0, 0, 32, 16));
201 let mascot = RatatuiMascot::new().set_eye(MascotEyeColor::Red);
202 mascot.render(buf.area, &mut buf);
203 assert_eq!(mascot.eye_state, MascotEyeColor::Red);
204 assert_eq!(buf[(21, 5)].bg, Color::Indexed(196));
205 }
206
207 #[test]
208 fn render_mascot() {
209 let mascot = RatatuiMascot::new();
210 let mut buf = Buffer::empty(Rect::new(0, 0, 32, 16));
211 mascot.render(buf.area, &mut buf);
212 assert_eq!(buf.area.as_size(), (32, 16).into());
213 assert_eq!(buf[(21, 5)].bg, Color::Indexed(236));
214 assert_eq!(
215 buf.content
216 .iter()
217 .map(ratatui_core::buffer::Cell::symbol)
218 .collect::<String>(),
219 Buffer::with_lines([
220 " ▄▄███ ",
221 " ▄███████ ",
222 " ▄█████████ ",
223 " ████████████ ",
224 " ▀███████████▀ ▄▄██████",
225 " ▀███▀▄█▀▀████████ ",
226 " ▄▄▄▄▀▄████████████ ",
227 " ████████████████ ",
228 " ▀███▀██████████ ",
229 " ▄▀▀▄ █████████ ",
230 " ▄▀ ▄ ▀▄▀█████████ ",
231 " ▄▀ ▀▀ ▀▄▀███████ ",
232 " ▄▀ ▄▄ ▀▄▀█████████ ",
233 " ▄▀ ▀▀ ▀▄▀██▀ ███ ",
234 "█ ▀▄▀ ▄██ ",
235 " ▀▄ ▀▄▀█ ",
236 ])
237 .content
238 .iter()
239 .map(ratatui_core::buffer::Cell::symbol)
240 .collect::<String>()
241 );
242 }
243
244 #[test]
245 fn render_in_minimal_buffer() {
246 let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
247 let mascot = RatatuiMascot::new();
248 mascot.render(buffer.area, &mut buffer);
250 assert_eq!(buffer, Buffer::with_lines([" "]));
251 }
252
253 #[test]
254 fn render_in_zero_size_buffer() {
255 let mut buffer = Buffer::empty(Rect::ZERO);
256 let mascot = RatatuiMascot::new();
257 mascot.render(buffer.area, &mut buffer);
259 }
260}