Skip to main content

blizz_ui/components/
text.rs

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