use std::io::Write;
use crossterm::{
cursor::MoveTo,
queue,
style::{Attribute, Color, Print, ResetColor, SetAttribute, SetForegroundColor},
};
use rand::Rng;
use crate::decode;
use crate::layout::centered_column;
pub struct TextComponent {
pub text: String,
pub reveal: f64,
pub visible: bool,
}
impl TextComponent {
pub fn new(text: impl Into<String>) -> Self {
Self {
text: text.into(),
reveal: 0.0,
visible: true,
}
}
#[cfg(not(tarpaulin_include))]
pub fn render<W: Write, R: Rng>(
&self,
writer: &mut W,
tw: u16,
row: u16,
rng: &mut R,
) -> std::io::Result<()> {
if !self.visible || self.reveal <= 0.0 {
return Ok(());
}
let text = &self.text;
let display = if self.reveal >= 1.0 {
text.clone()
} else {
let revealed = (text.chars().count() as f64 * self.reveal).round() as usize;
decode::decode_frame(text, revealed, rng)
};
let col = centered_column(tw, text.chars().count() as u16);
queue!(
writer,
MoveTo(col, row),
SetForegroundColor(Color::White),
SetAttribute(Attribute::Bold),
Print(&display),
SetAttribute(Attribute::Reset),
ResetColor
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use rand::SeedableRng;
use rand::rngs::SmallRng;
fn seeded_rng() -> SmallRng {
SmallRng::seed_from_u64(123)
}
#[test]
fn render_skips_when_invisible() {
let mut t = TextComponent::new("hi");
t.visible = false;
let mut buf = Vec::new();
t.render(&mut buf, 80, 0, &mut seeded_rng()).unwrap();
assert!(buf.is_empty());
}
#[test]
fn render_skips_when_reveal_zero() {
let t = TextComponent::new("hi");
let mut buf = Vec::new();
t.render(&mut buf, 80, 0, &mut seeded_rng()).unwrap();
assert!(buf.is_empty());
}
#[test]
fn render_full_reveal_writes_text() {
let mut t = TextComponent::new("hello");
t.reveal = 1.0;
let mut buf = Vec::new();
t.render(&mut buf, 80, 5, &mut seeded_rng()).unwrap();
let s = String::from_utf8(buf).unwrap();
assert!(s.contains("hello"));
}
#[test]
fn render_partial_writes_output() {
let mut t = TextComponent::new("hello world");
t.reveal = 0.3;
let mut buf = Vec::new();
t.render(&mut buf, 80, 5, &mut seeded_rng()).unwrap();
assert!(
!buf.is_empty(),
"partial reveal should emit terminal sequences"
);
}
#[test]
fn new_sets_visible_defaults() {
let t = TextComponent::new("q");
assert!(t.visible);
assert_eq!(t.reveal, 0.0);
assert_eq!(t.text, "q");
}
}