blizz_ui/components/
text_entry.rs1use 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 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 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 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 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 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 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}