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  /// Wizard decode phase: grows the chrome and reveals labels after the question is half-shown.
49  pub fn tick_decode(&mut self, question_reveal: f64, box_open_speed: f64, reveal_speed: f64) {
50    if !self.visible || question_reveal < 0.5 {
51      return;
52    }
53    self.open = (self.open + box_open_speed).min(1.0);
54    if self.open >= 1.0 {
55      self.label_reveal = (self.label_reveal + reveal_speed).min(1.0);
56      self.content_reveal = (self.content_reveal + reveal_speed).min(1.0);
57    }
58  }
59
60  /// Transition encode: scrub label/content; optionally shrink the border when leaving prompt steps.
61  pub fn tick_transition_encode(
62    &mut self,
63    next_has_prompt_box: bool,
64    encode_speed: f64,
65    morph_speed: f64,
66  ) {
67    if !self.visible {
68      return;
69    }
70    self.label_reveal = (self.label_reveal - encode_speed).max(0.0);
71    self.content_reveal = (self.content_reveal - encode_speed).max(0.0);
72    let text_hidden = self.label_reveal <= 0.0 && self.content_reveal <= 0.0;
73    if !next_has_prompt_box && text_hidden {
74      self.open = (self.open - morph_speed).max(0.0);
75    }
76  }
77
78  /// Transition decode after `swap_to`: open chrome then fade labels/content in.
79  pub fn tick_transition_decode(&mut self, encode_speed: f64, morph_speed: f64) {
80    if !self.visible {
81      return;
82    }
83    if self.open < 1.0 {
84      self.open = (self.open + morph_speed).min(1.0);
85    } else {
86      self.label_reveal = (self.label_reveal + encode_speed).min(1.0);
87      self.content_reveal = (self.content_reveal + encode_speed).min(1.0);
88    }
89  }
90
91  /// Exit phase: encode away text then close chrome.
92  pub fn tick_exit_close(&mut self, encode_speed: f64, box_close_speed: f64) {
93    if !self.visible {
94      return;
95    }
96    self.label_reveal = (self.label_reveal - encode_speed).max(0.0);
97    self.content_reveal = (self.content_reveal - encode_speed).max(0.0);
98    if self.label_reveal <= 0.0 && self.content_reveal <= 0.0 {
99      self.open = (self.open - box_close_speed).max(0.0);
100    }
101  }
102
103  #[cfg(not(tarpaulin_include))]
104  pub fn render_with_cursor<W: Write, R: Rng>(
105    &self,
106    writer: &mut W,
107    layout: &TextEntryLayout,
108    tw: u16,
109    buf: &InputBuffer,
110    rng: &mut R,
111  ) -> std::io::Result<u16> {
112    if self.open < 1.0 {
113      self.render_partial_open(writer, layout, tw)?;
114      return Ok(0);
115    }
116
117    let label_display = if self.label_reveal >= 1.0 {
118      self.label.clone()
119    } else {
120      let revealed = (self.label.chars().count() as f64 * self.label_reveal).round() as usize;
121      decode::decode_frame(&self.label, revealed, rng)
122    };
123
124    let content_display = if self.content_reveal >= 1.0 {
125      self.content.clone()
126    } else {
127      let revealed = (self.content.chars().count() as f64 * self.content_reveal).round() as usize;
128      decode::decode_frame(&self.content, revealed, rng)
129    };
130
131    let frame = text_entry(&label_display, &content_display, tw);
132    let label_width = label_display.chars().count() as u16;
133    let buf_text = input_buffer::text(buf);
134    let cursor_pos = input_buffer::cursor(buf);
135    let selection = input_buffer::selection_range(buf);
136    queue_text_entry_with_cursor(
137      writer,
138      layout,
139      &frame,
140      buf_text,
141      label_width,
142      cursor_pos,
143      selection,
144    )
145  }
146
147  #[cfg(not(tarpaulin_include))]
148  pub fn render_partial_open<W: Write>(
149    &self,
150    writer: &mut W,
151    layout: &TextEntryLayout,
152    tw: u16,
153  ) -> std::io::Result<()> {
154    let full_width = layout.inner_width;
155    let raw = ((full_width as f64) * self.open).round() as u16;
156    let even = (raw / 2) * 2;
157    let current_width = if even == 0 && raw > 0 {
158      2
159    } else {
160      even.min(full_width)
161    };
162
163    if current_width == 0 {
164      return queue_clear_prompt(writer, layout);
165    }
166
167    let partial = TextEntry {
168      label: String::new(),
169      hint: String::new(),
170      inner_width: current_width,
171    };
172    let partial_layout =
173      text_entry_layout(&partial, layout_mod::size(tw, layout.question_row + 10));
174    let merged = TextEntryLayout {
175      question_row: layout.question_row,
176      box_top_row: layout.box_top_row,
177      content_row: layout.content_row,
178      box_bottom_row: layout.box_bottom_row,
179      hint_row: layout.hint_row,
180      box_column: partial_layout.box_column,
181      inner_width: current_width,
182    };
183    queue_text_entry(writer, &merged, &partial, "", 0)
184  }
185}