blizz_ui/components/
text.rs1use 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
13pub 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}