1#[derive(Debug, Clone, Default)]
5pub struct StaticOutput {
6 lines: Vec<String>,
7 new_lines: Vec<String>,
8}
9
10impl StaticOutput {
11 pub fn new() -> Self {
13 Self::default()
14 }
15
16 pub fn println(&mut self, line: impl Into<String>) {
18 let line = line.into();
19 self.lines.push(line.clone());
20 self.new_lines.push(line);
21 }
22
23 pub fn lines(&self) -> &[String] {
25 &self.lines
26 }
27
28 pub fn drain_new(&mut self) -> Vec<String> {
30 std::mem::take(&mut self.new_lines)
31 }
32
33 pub fn clear(&mut self) {
35 self.lines.clear();
36 self.new_lines.clear();
37 }
38}
39
40pub struct TextInputState {
56 pub value: String,
58 pub cursor: usize,
60 pub placeholder: String,
62 pub max_length: Option<usize>,
64 pub validation_error: Option<String>,
66 pub masked: bool,
68 pub suggestions: Vec<String>,
70 pub suggestion_index: usize,
72 pub show_suggestions: bool,
74 validators: Vec<TextInputValidator>,
76 validation_errors: Vec<String>,
78}
79
80impl std::fmt::Debug for TextInputState {
81 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82 f.debug_struct("TextInputState")
83 .field("value", &self.value)
84 .field("cursor", &self.cursor)
85 .field("placeholder", &self.placeholder)
86 .field("max_length", &self.max_length)
87 .field("validation_error", &self.validation_error)
88 .field("masked", &self.masked)
89 .field("suggestions", &self.suggestions)
90 .field("suggestion_index", &self.suggestion_index)
91 .field("show_suggestions", &self.show_suggestions)
92 .field("validators_len", &self.validators.len())
93 .field("validation_errors", &self.validation_errors)
94 .finish()
95 }
96}
97
98impl Clone for TextInputState {
99 fn clone(&self) -> Self {
100 Self {
101 value: self.value.clone(),
102 cursor: self.cursor,
103 placeholder: self.placeholder.clone(),
104 max_length: self.max_length,
105 validation_error: self.validation_error.clone(),
106 masked: self.masked,
107 suggestions: self.suggestions.clone(),
108 suggestion_index: self.suggestion_index,
109 show_suggestions: self.show_suggestions,
110 validators: Vec::new(),
111 validation_errors: self.validation_errors.clone(),
112 }
113 }
114}
115
116impl TextInputState {
117 pub fn new() -> Self {
119 Self {
120 value: String::new(),
121 cursor: 0,
122 placeholder: String::new(),
123 max_length: None,
124 validation_error: None,
125 masked: false,
126 suggestions: Vec::new(),
127 suggestion_index: 0,
128 show_suggestions: false,
129 validators: Vec::new(),
130 validation_errors: Vec::new(),
131 }
132 }
133
134 pub fn with_placeholder(p: impl Into<String>) -> Self {
136 Self {
137 placeholder: p.into(),
138 ..Self::new()
139 }
140 }
141
142 pub fn max_length(mut self, len: usize) -> Self {
144 self.max_length = Some(len);
145 self
146 }
147
148 pub fn validate(&mut self, validator: impl Fn(&str) -> Result<(), String>) {
156 self.validation_error = validator(&self.value).err();
157 }
158
159 pub fn add_validator(&mut self, f: impl Fn(&str) -> Result<(), String> + 'static) {
164 self.validators.push(Box::new(f));
165 }
166
167 pub fn run_validators(&mut self) {
172 self.validation_errors.clear();
173 for validator in &self.validators {
174 if let Err(err) = validator(&self.value) {
175 self.validation_errors.push(err);
176 }
177 }
178 self.validation_error = self.validation_errors.first().cloned();
179 }
180
181 pub fn errors(&self) -> &[String] {
183 &self.validation_errors
184 }
185
186 pub fn set_suggestions(&mut self, suggestions: Vec<String>) {
188 self.suggestions = suggestions;
189 self.suggestion_index = 0;
190 self.show_suggestions = !self.suggestions.is_empty();
191 }
192
193 pub fn matched_suggestions(&self) -> Vec<&str> {
195 if self.value.is_empty() {
196 return Vec::new();
197 }
198 let lower = self.value.to_lowercase();
199 self.suggestions
200 .iter()
201 .filter(|s| s.to_lowercase().starts_with(&lower))
202 .map(|s| s.as_str())
203 .collect()
204 }
205}
206
207impl Default for TextInputState {
208 fn default() -> Self {
209 Self::new()
210 }
211}
212
213#[derive(Debug, Default)]
215pub struct FormField {
216 pub label: String,
218 pub input: TextInputState,
220 pub error: Option<String>,
222}
223
224impl FormField {
225 pub fn new(label: impl Into<String>) -> Self {
227 Self {
228 label: label.into(),
229 input: TextInputState::new(),
230 error: None,
231 }
232 }
233
234 pub fn placeholder(mut self, p: impl Into<String>) -> Self {
236 self.input.placeholder = p.into();
237 self
238 }
239}
240
241#[derive(Debug)]
243pub struct FormState {
244 pub fields: Vec<FormField>,
246 pub submitted: bool,
248}
249
250impl FormState {
251 pub fn new() -> Self {
253 Self {
254 fields: Vec::new(),
255 submitted: false,
256 }
257 }
258
259 pub fn field(mut self, field: FormField) -> Self {
261 self.fields.push(field);
262 self
263 }
264
265 pub fn validate(&mut self, validators: &[FormValidator]) -> bool {
269 let mut all_valid = true;
270 for (i, field) in self.fields.iter_mut().enumerate() {
271 if let Some(validator) = validators.get(i) {
272 match validator(&field.input.value) {
273 Ok(()) => field.error = None,
274 Err(msg) => {
275 field.error = Some(msg);
276 all_valid = false;
277 }
278 }
279 }
280 }
281 all_valid
282 }
283
284 pub fn value(&self, index: usize) -> &str {
286 self.fields
287 .get(index)
288 .map(|f| f.input.value.as_str())
289 .unwrap_or("")
290 }
291}
292
293impl Default for FormState {
294 fn default() -> Self {
295 Self::new()
296 }
297}
298
299#[derive(Debug, Clone)]
305pub struct ToastState {
306 pub messages: Vec<ToastMessage>,
308}
309
310#[derive(Debug, Clone)]
312pub struct ToastMessage {
313 pub text: String,
315 pub level: ToastLevel,
317 pub created_tick: u64,
319 pub duration_ticks: u64,
321}
322
323impl Default for ToastMessage {
324 fn default() -> Self {
325 Self {
326 text: String::new(),
327 level: ToastLevel::Info,
328 created_tick: 0,
329 duration_ticks: 30,
330 }
331 }
332}
333
334#[derive(Debug, Clone, Copy, PartialEq, Eq)]
336pub enum ToastLevel {
337 Info,
339 Success,
341 Warning,
343 Error,
345}
346
347#[non_exhaustive]
349#[derive(Debug, Clone, Copy, PartialEq, Eq)]
350pub enum AlertLevel {
351 Info,
353 Success,
355 Warning,
357 Error,
359}
360
361impl ToastState {
362 pub fn new() -> Self {
364 Self {
365 messages: Vec::new(),
366 }
367 }
368
369 pub fn info(&mut self, text: impl Into<String>, tick: u64) {
371 self.push(text, ToastLevel::Info, tick, 30);
372 }
373
374 pub fn success(&mut self, text: impl Into<String>, tick: u64) {
376 self.push(text, ToastLevel::Success, tick, 30);
377 }
378
379 pub fn warning(&mut self, text: impl Into<String>, tick: u64) {
381 self.push(text, ToastLevel::Warning, tick, 50);
382 }
383
384 pub fn error(&mut self, text: impl Into<String>, tick: u64) {
386 self.push(text, ToastLevel::Error, tick, 80);
387 }
388
389 pub fn push(
391 &mut self,
392 text: impl Into<String>,
393 level: ToastLevel,
394 tick: u64,
395 duration_ticks: u64,
396 ) {
397 self.messages.push(ToastMessage {
398 text: text.into(),
399 level,
400 created_tick: tick,
401 duration_ticks,
402 });
403 }
404
405 pub fn cleanup(&mut self, current_tick: u64) {
409 self.messages.retain(|message| {
410 current_tick < message.created_tick.saturating_add(message.duration_ticks)
411 });
412 }
413}
414
415impl Default for ToastState {
416 fn default() -> Self {
417 Self::new()
418 }
419}
420
421#[derive(Debug, Clone)]
426pub struct TextareaState {
427 pub lines: Vec<String>,
429 pub cursor_row: usize,
431 pub cursor_col: usize,
433 pub max_length: Option<usize>,
435 pub wrap_width: Option<u32>,
437 pub scroll_offset: usize,
439}
440
441impl TextareaState {
442 pub fn new() -> Self {
444 Self {
445 lines: vec![String::new()],
446 cursor_row: 0,
447 cursor_col: 0,
448 max_length: None,
449 wrap_width: None,
450 scroll_offset: 0,
451 }
452 }
453
454 pub fn value(&self) -> String {
456 self.lines.join("\n")
457 }
458
459 pub fn set_value(&mut self, text: impl Into<String>) {
463 let value = text.into();
464 self.lines = value.split('\n').map(str::to_string).collect();
465 if self.lines.is_empty() {
466 self.lines.push(String::new());
467 }
468 self.cursor_row = 0;
469 self.cursor_col = 0;
470 self.scroll_offset = 0;
471 }
472
473 pub fn max_length(mut self, len: usize) -> Self {
475 self.max_length = Some(len);
476 self
477 }
478
479 pub fn word_wrap(mut self, width: u32) -> Self {
481 self.wrap_width = Some(width);
482 self
483 }
484}
485
486impl Default for TextareaState {
487 fn default() -> Self {
488 Self::new()
489 }
490}
491
492#[derive(Debug, Clone)]
498pub struct SpinnerState {
499 chars: Vec<char>,
500}
501
502impl SpinnerState {
503 pub fn dots() -> Self {
507 Self {
508 chars: vec!['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'],
509 }
510 }
511
512 pub fn line() -> Self {
516 Self {
517 chars: vec!['|', '/', '-', '\\'],
518 }
519 }
520
521 pub fn frame(&self, tick: u64) -> char {
523 if self.chars.is_empty() {
524 return ' ';
525 }
526 self.chars[tick as usize % self.chars.len()]
527 }
528}
529
530impl Default for SpinnerState {
531 fn default() -> Self {
532 Self::dots()
533 }
534}