Skip to main content

blizz_ui/components/
text.rs

1use std::io::Write;
2
3use crossterm::{
4  cursor::MoveTo,
5  queue,
6  style::{Attribute, Color, Print, ResetColor, SetAttribute, SetForegroundColor},
7};
8use rand::Rng;
9
10use crate::decode;
11use crate::layout::centered_column;
12
13/// Centered question / message text with progressive decode reveal.
14pub struct TextComponent {
15  pub text: String,
16  pub reveal: f64,
17  pub visible: bool,
18}
19
20impl TextComponent {
21  pub fn new(text: impl Into<String>) -> Self {
22    Self {
23      text: text.into(),
24      reveal: 0.0,
25      visible: true,
26    }
27  }
28
29  #[cfg(not(tarpaulin_include))]
30  pub fn render<W: Write, R: Rng>(
31    &self,
32    writer: &mut W,
33    tw: u16,
34    row: u16,
35    rng: &mut R,
36  ) -> std::io::Result<()> {
37    if !self.visible || self.reveal <= 0.0 {
38      return Ok(());
39    }
40
41    let text = &self.text;
42    let display = if self.reveal >= 1.0 {
43      text.clone()
44    } else {
45      let revealed = (text.chars().count() as f64 * self.reveal).round() as usize;
46      decode::decode_frame(text, revealed, rng)
47    };
48
49    let col = centered_column(tw, text.chars().count() as u16);
50    queue!(
51      writer,
52      MoveTo(col, row),
53      SetForegroundColor(Color::White),
54      SetAttribute(Attribute::Bold),
55      Print(&display),
56      SetAttribute(Attribute::Reset),
57      ResetColor
58    )
59  }
60}
61
62#[cfg(test)]
63mod tests {
64  use super::*;
65  use rand::SeedableRng;
66  use rand::rngs::SmallRng;
67
68  fn seeded_rng() -> SmallRng {
69    SmallRng::seed_from_u64(123)
70  }
71
72  #[test]
73  fn render_skips_when_invisible() {
74    let mut t = TextComponent::new("hi");
75    t.visible = false;
76    let mut buf = Vec::new();
77    t.render(&mut buf, 80, 0, &mut seeded_rng()).unwrap();
78    assert!(buf.is_empty());
79  }
80
81  #[test]
82  fn render_skips_when_reveal_zero() {
83    let t = TextComponent::new("hi");
84    let mut buf = Vec::new();
85    t.render(&mut buf, 80, 0, &mut seeded_rng()).unwrap();
86    assert!(buf.is_empty());
87  }
88
89  #[test]
90  fn render_full_reveal_writes_text() {
91    let mut t = TextComponent::new("hello");
92    t.reveal = 1.0;
93    let mut buf = Vec::new();
94    t.render(&mut buf, 80, 5, &mut seeded_rng()).unwrap();
95    let s = String::from_utf8(buf).unwrap();
96    assert!(s.contains("hello"));
97  }
98
99  #[test]
100  fn render_partial_writes_output() {
101    let mut t = TextComponent::new("hello world");
102    t.reveal = 0.3;
103    let mut buf = Vec::new();
104    t.render(&mut buf, 80, 5, &mut seeded_rng()).unwrap();
105    assert!(
106      !buf.is_empty(),
107      "partial reveal should emit terminal sequences"
108    );
109  }
110
111  #[test]
112  fn new_sets_visible_defaults() {
113    let t = TextComponent::new("q");
114    assert!(t.visible);
115    assert_eq!(t.reveal, 0.0);
116    assert_eq!(t.text, "q");
117  }
118}