blizz_ui/components/mascot/
frames.rs1use std::{io::Write, sync::OnceLock};
2
3use crossterm::{
4 cursor::MoveTo,
5 queue,
6 style::{Print, ResetColor},
7};
8
9use crate::layout::{Position, Size, centered_block_origin, text_block_size, top_two_thirds};
10
11pub const MASCOT: &str = include_str!("mascot.txt");
12
13static MASCOT_FRAME: OnceLock<Vec<&'static str>> = OnceLock::new();
14
15#[derive(Debug, Clone, Copy)]
16pub struct MascotFrames {
17 frame: &'static [&'static str],
18}
19
20impl MascotFrames {
21 pub fn new(frame: &'static [&'static str]) -> Self {
22 Self { frame }
23 }
24
25 pub fn blizz() -> Self {
26 Self::new(lines())
27 }
28
29 pub fn lines(&self) -> &'static [&'static str] {
30 self.frame
31 }
32
33 pub fn size(&self) -> Size {
34 text_block_size(self.frame)
35 }
36
37 pub fn centered_origin(&self, terminal_size: Size) -> Position {
38 centered_block_origin(top_two_thirds(terminal_size), self.size())
39 }
40
41 pub fn queue_at<W: Write>(&self, writer: &mut W, origin: Position) -> std::io::Result<()> {
42 queue_frame(writer, origin, self.frame)
43 }
44
45 pub fn queue_centered<W: Write>(
46 &self,
47 writer: &mut W,
48 terminal_size: Size,
49 ) -> std::io::Result<()> {
50 self.queue_at(writer, self.centered_origin(terminal_size))
51 }
52}
53
54impl Default for MascotFrames {
55 fn default() -> Self {
56 Self::blizz()
57 }
58}
59
60pub fn lines() -> &'static [&'static str] {
61 MASCOT_FRAME.get_or_init(|| block_lines(MASCOT)).as_slice()
62}
63
64pub fn size() -> Size {
65 MascotFrames::blizz().size()
66}
67
68pub fn centered_origin(terminal_size: Size) -> Position {
69 MascotFrames::blizz().centered_origin(terminal_size)
70}
71
72pub fn queue_frame<W: Write>(
73 writer: &mut W,
74 origin: Position,
75 lines: &[&str],
76) -> std::io::Result<()> {
77 for (offset, line) in lines.iter().enumerate() {
78 queue!(
79 writer,
80 MoveTo(origin.column, origin.row.saturating_add(offset as u16)),
81 Print(line)
82 )?;
83 }
84
85 queue!(writer, ResetColor)?;
86 Ok(())
87}
88
89pub fn queue_frame_owned<W: Write>(
90 writer: &mut W,
91 origin: Position,
92 lines: &[String],
93) -> std::io::Result<()> {
94 for (offset, line) in lines.iter().enumerate() {
95 queue!(
96 writer,
97 MoveTo(origin.column, origin.row.saturating_add(offset as u16)),
98 Print(line)
99 )?;
100 }
101
102 queue!(writer, ResetColor)?;
103 Ok(())
104}
105
106pub fn queue_centered<W: Write>(writer: &mut W, terminal_size: Size) -> std::io::Result<()> {
107 MascotFrames::blizz().queue_centered(writer, terminal_size)
108}
109
110fn block_lines(block: &'static str) -> Vec<&'static str> {
111 block.trim_end_matches('\n').lines().collect()
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117 use crate::layout;
118
119 #[test]
120 fn mascot_has_static_dimensions() {
121 assert_eq!(MascotFrames::blizz().size(), layout::size(38, 30));
122 assert_eq!(size(), layout::size(38, 30));
123 }
124
125 #[test]
126 fn centered_origin_uses_top_two_thirds() {
127 assert_eq!(
128 MascotFrames::blizz().centered_origin(layout::size(100, 60)),
129 layout::position(31, 5)
130 );
131 assert_eq!(
132 centered_origin(layout::size(100, 60)),
133 layout::position(31, 5)
134 );
135 }
136
137 #[test]
138 fn default_frames_use_blizz_mascot() {
139 assert_eq!(MascotFrames::default().lines(), lines());
140 }
141
142 #[test]
143 fn queue_centered_writes_mascot() {
144 let mut buffer = Vec::new();
145
146 queue_centered(&mut buffer, layout::size(100, 60)).unwrap();
147 let output = String::from_utf8(buffer).unwrap();
148
149 assert!(output.contains("▓"));
150 }
151
152 #[test]
153 fn queue_frame_writes_all_lines() {
154 let mut buffer = Vec::new();
155
156 queue_frame(&mut buffer, layout::position(0, 0), &["a", "b"]).unwrap();
157 let output = String::from_utf8(buffer).unwrap();
158
159 assert!(output.contains("a"));
160 assert!(output.contains("b"));
161 }
162
163 #[test]
164 fn queue_frame_owned_writes_owned_strings() {
165 let mut buffer = Vec::new();
166 let lines = vec!["hello".to_string(), "world".to_string()];
167
168 queue_frame_owned(&mut buffer, layout::position(0, 0), &lines).unwrap();
169 let output = String::from_utf8(buffer).unwrap();
170
171 assert!(output.contains("hello"));
172 assert!(output.contains("world"));
173 }
174
175 #[test]
176 fn queue_at_writes_at_position() {
177 let mut buffer = Vec::new();
178 let frames = MascotFrames::blizz();
179
180 frames
181 .queue_at(&mut buffer, layout::position(5, 3))
182 .unwrap();
183
184 assert!(!buffer.is_empty());
185 }
186
187 #[test]
188 fn new_constructs_from_static_slice() {
189 static LINES: &[&str] = &["abc", "def"];
190 let frames = MascotFrames::new(LINES);
191
192 assert_eq!(frames.lines(), LINES);
193 assert_eq!(frames.size(), layout::size(3, 2));
194 }
195
196 #[test]
197 fn block_lines_splits_on_newlines() {
198 let result = block_lines("a\nb\nc\n");
199 assert_eq!(result, vec!["a", "b", "c"]);
200 }
201}