Skip to main content

blizz_ui/components/mascot/
frames.rs

1use 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}