#[derive(Debug, Clone, Default)]
pub struct StaticOutput {
lines: Vec<String>,
new_lines: Vec<String>,
}
impl StaticOutput {
pub fn new() -> Self {
Self::default()
}
pub fn println(&mut self, line: impl Into<String>) {
let line = line.into();
self.lines.push(line.clone());
self.new_lines.push(line);
}
pub fn lines(&self) -> &[String] {
&self.lines
}
pub fn drain_new(&mut self) -> Vec<String> {
std::mem::take(&mut self.new_lines)
}
pub fn clear(&mut self) {
self.lines.clear();
self.new_lines.clear();
}
}
pub struct TextInputState {
pub value: String,
pub cursor: usize,
pub placeholder: String,
pub max_length: Option<usize>,
pub validation_error: Option<String>,
pub masked: bool,
pub suggestions: Vec<String>,
pub suggestion_index: usize,
pub show_suggestions: bool,
validators: Vec<TextInputValidator>,
validation_errors: Vec<String>,
}
impl std::fmt::Debug for TextInputState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TextInputState")
.field("value", &self.value)
.field("cursor", &self.cursor)
.field("placeholder", &self.placeholder)
.field("max_length", &self.max_length)
.field("validation_error", &self.validation_error)
.field("masked", &self.masked)
.field("suggestions", &self.suggestions)
.field("suggestion_index", &self.suggestion_index)
.field("show_suggestions", &self.show_suggestions)
.field("validators_len", &self.validators.len())
.field("validation_errors", &self.validation_errors)
.finish()
}
}
impl Clone for TextInputState {
fn clone(&self) -> Self {
Self {
value: self.value.clone(),
cursor: self.cursor,
placeholder: self.placeholder.clone(),
max_length: self.max_length,
validation_error: self.validation_error.clone(),
masked: self.masked,
suggestions: self.suggestions.clone(),
suggestion_index: self.suggestion_index,
show_suggestions: self.show_suggestions,
validators: Vec::new(),
validation_errors: self.validation_errors.clone(),
}
}
}
impl TextInputState {
pub fn new() -> Self {
Self {
value: String::new(),
cursor: 0,
placeholder: String::new(),
max_length: None,
validation_error: None,
masked: false,
suggestions: Vec::new(),
suggestion_index: 0,
show_suggestions: false,
validators: Vec::new(),
validation_errors: Vec::new(),
}
}
pub fn with_placeholder(p: impl Into<String>) -> Self {
Self {
placeholder: p.into(),
..Self::new()
}
}
pub fn max_length(mut self, len: usize) -> Self {
self.max_length = Some(len);
self
}
pub fn validate(&mut self, validator: impl Fn(&str) -> Result<(), String>) {
self.validation_error = validator(&self.value).err();
}
pub fn add_validator(&mut self, f: impl Fn(&str) -> Result<(), String> + 'static) {
self.validators.push(Box::new(f));
}
pub fn run_validators(&mut self) {
self.validation_errors.clear();
for validator in &self.validators {
if let Err(err) = validator(&self.value) {
self.validation_errors.push(err);
}
}
self.validation_error = self.validation_errors.first().cloned();
}
pub fn errors(&self) -> &[String] {
&self.validation_errors
}
pub fn set_suggestions(&mut self, suggestions: Vec<String>) {
self.suggestions = suggestions;
self.suggestion_index = 0;
self.show_suggestions = !self.suggestions.is_empty();
}
pub fn matched_suggestions(&self) -> Vec<&str> {
if self.value.is_empty() {
return Vec::new();
}
let lower = self.value.to_lowercase();
self.suggestions
.iter()
.filter(|s| s.to_lowercase().starts_with(&lower))
.map(|s| s.as_str())
.collect()
}
}
impl Default for TextInputState {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Default)]
pub struct FormField {
pub label: String,
pub input: TextInputState,
pub error: Option<String>,
}
impl FormField {
pub fn new(label: impl Into<String>) -> Self {
Self {
label: label.into(),
input: TextInputState::new(),
error: None,
}
}
pub fn placeholder(mut self, p: impl Into<String>) -> Self {
self.input.placeholder = p.into();
self
}
}
#[derive(Debug)]
pub struct FormState {
pub fields: Vec<FormField>,
pub submitted: bool,
}
impl FormState {
pub fn new() -> Self {
Self {
fields: Vec::new(),
submitted: false,
}
}
pub fn field(mut self, field: FormField) -> Self {
self.fields.push(field);
self
}
pub fn validate(&mut self, validators: &[FormValidator]) -> bool {
let mut all_valid = true;
for (i, field) in self.fields.iter_mut().enumerate() {
if let Some(validator) = validators.get(i) {
match validator(&field.input.value) {
Ok(()) => field.error = None,
Err(msg) => {
field.error = Some(msg);
all_valid = false;
}
}
}
}
all_valid
}
pub fn value(&self, index: usize) -> &str {
self.fields
.get(index)
.map(|f| f.input.value.as_str())
.unwrap_or("")
}
}
impl Default for FormState {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct ToastState {
pub messages: Vec<ToastMessage>,
}
#[derive(Debug, Clone)]
pub struct ToastMessage {
pub text: String,
pub level: ToastLevel,
pub created_tick: u64,
pub duration_ticks: u64,
}
impl Default for ToastMessage {
fn default() -> Self {
Self {
text: String::new(),
level: ToastLevel::Info,
created_tick: 0,
duration_ticks: 30,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ToastLevel {
Info,
Success,
Warning,
Error,
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AlertLevel {
Info,
Success,
Warning,
Error,
}
impl ToastState {
pub fn new() -> Self {
Self {
messages: Vec::new(),
}
}
pub fn info(&mut self, text: impl Into<String>, tick: u64) {
self.push(text, ToastLevel::Info, tick, 30);
}
pub fn success(&mut self, text: impl Into<String>, tick: u64) {
self.push(text, ToastLevel::Success, tick, 30);
}
pub fn warning(&mut self, text: impl Into<String>, tick: u64) {
self.push(text, ToastLevel::Warning, tick, 50);
}
pub fn error(&mut self, text: impl Into<String>, tick: u64) {
self.push(text, ToastLevel::Error, tick, 80);
}
pub fn push(
&mut self,
text: impl Into<String>,
level: ToastLevel,
tick: u64,
duration_ticks: u64,
) {
self.messages.push(ToastMessage {
text: text.into(),
level,
created_tick: tick,
duration_ticks,
});
}
pub fn cleanup(&mut self, current_tick: u64) {
self.messages.retain(|message| {
current_tick < message.created_tick.saturating_add(message.duration_ticks)
});
}
}
impl Default for ToastState {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct TextareaState {
pub lines: Vec<String>,
pub cursor_row: usize,
pub cursor_col: usize,
pub max_length: Option<usize>,
pub wrap_width: Option<u32>,
pub scroll_offset: usize,
}
impl TextareaState {
pub fn new() -> Self {
Self {
lines: vec![String::new()],
cursor_row: 0,
cursor_col: 0,
max_length: None,
wrap_width: None,
scroll_offset: 0,
}
}
pub fn value(&self) -> String {
self.lines.join("\n")
}
pub fn set_value(&mut self, text: impl Into<String>) {
let value = text.into();
self.lines = value.split('\n').map(str::to_string).collect();
if self.lines.is_empty() {
self.lines.push(String::new());
}
self.cursor_row = 0;
self.cursor_col = 0;
self.scroll_offset = 0;
}
pub fn max_length(mut self, len: usize) -> Self {
self.max_length = Some(len);
self
}
pub fn word_wrap(mut self, width: u32) -> Self {
self.wrap_width = Some(width);
self
}
}
impl Default for TextareaState {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct SpinnerState {
chars: Vec<char>,
}
impl SpinnerState {
pub fn dots() -> Self {
Self {
chars: vec!['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'],
}
}
pub fn line() -> Self {
Self {
chars: vec!['|', '/', '-', '\\'],
}
}
pub fn frame(&self, tick: u64) -> char {
if self.chars.is_empty() {
return ' ';
}
self.chars[tick as usize % self.chars.len()]
}
}
impl Default for SpinnerState {
fn default() -> Self {
Self::dots()
}
}