Skip to main content

blizz_ui/components/
text_entry.rs

1use std::io::Write;
2
3use rand::Rng;
4
5use crate::decode;
6use crate::input_buffer::{self, InputBuffer};
7use crate::layout as layout_mod;
8use crate::prompt::{
9  TextEntry, TextEntryLayout, queue_clear_prompt, queue_text_entry, queue_text_entry_with_cursor,
10  text_entry, text_entry_layout,
11};
12
13#[derive(Debug, Clone)]
14pub struct TextEntryComponent {
15  pub label: String,
16  pub content: String,
17  pub open: f64,
18  pub label_reveal: f64,
19  pub content_reveal: f64,
20  pub visible: bool,
21}
22
23impl TextEntryComponent {
24  /// Text entry chrome for an env-var style prompt (`label` = variable name, `content` = default).
25  pub fn prompt(label: impl Into<String>, default_value: impl Into<String>) -> Self {
26    Self {
27      label: label.into(),
28      content: default_value.into(),
29      open: 0.0,
30      label_reveal: 0.0,
31      content_reveal: 0.0,
32      visible: true,
33    }
34  }
35
36  /// Invisible shell when the step is dialog-only or selector-only (no editable field).
37  pub fn hidden() -> Self {
38    Self {
39      label: String::new(),
40      content: String::new(),
41      open: 0.0,
42      label_reveal: 0.0,
43      content_reveal: 0.0,
44      visible: false,
45    }
46  }
47
48  #[cfg(not(tarpaulin_include))]
49  pub fn render_with_cursor<W: Write, R: Rng>(
50    &self,
51    writer: &mut W,
52    layout: &TextEntryLayout,
53    tw: u16,
54    buf: &InputBuffer,
55    rng: &mut R,
56  ) -> std::io::Result<u16> {
57    if self.open < 1.0 {
58      self.render_partial_open(writer, layout, tw)?;
59      return Ok(0);
60    }
61
62    let label_display = if self.label_reveal >= 1.0 {
63      self.label.clone()
64    } else {
65      let revealed = (self.label.chars().count() as f64 * self.label_reveal).round() as usize;
66      decode::decode_frame(&self.label, revealed, rng)
67    };
68
69    let content_display = if self.content_reveal >= 1.0 {
70      self.content.clone()
71    } else {
72      let revealed = (self.content.chars().count() as f64 * self.content_reveal).round() as usize;
73      decode::decode_frame(&self.content, revealed, rng)
74    };
75
76    let frame = text_entry(&label_display, &content_display, tw);
77    let label_width = label_display.chars().count() as u16;
78    let buf_text = input_buffer::text(buf);
79    let cursor_pos = input_buffer::cursor(buf);
80    let selection = input_buffer::selection_range(buf);
81    queue_text_entry_with_cursor(
82      writer,
83      layout,
84      &frame,
85      buf_text,
86      label_width,
87      cursor_pos,
88      selection,
89    )
90  }
91
92  #[cfg(not(tarpaulin_include))]
93  pub fn render_partial_open<W: Write>(
94    &self,
95    writer: &mut W,
96    layout: &TextEntryLayout,
97    tw: u16,
98  ) -> std::io::Result<()> {
99    let full_width = layout.inner_width;
100    let raw = ((full_width as f64) * self.open).round() as u16;
101    let even = (raw / 2) * 2;
102    let current_width = if even == 0 && raw > 0 {
103      2
104    } else {
105      even.min(full_width)
106    };
107
108    if current_width == 0 {
109      return queue_clear_prompt(writer, layout);
110    }
111
112    let partial = TextEntry {
113      label: String::new(),
114      hint: String::new(),
115      inner_width: current_width,
116    };
117    let partial_layout =
118      text_entry_layout(&partial, layout_mod::size(tw, layout.question_row + 10));
119    let merged = TextEntryLayout {
120      question_row: layout.question_row,
121      box_top_row: layout.box_top_row,
122      content_row: layout.content_row,
123      box_bottom_row: layout.box_bottom_row,
124      hint_row: layout.hint_row,
125      box_column: partial_layout.box_column,
126      inner_width: current_width,
127    };
128    queue_text_entry(writer, &merged, &partial, "", 0)
129  }
130}