#![allow(clippy::iter_skip_next)]
use crate::style::Color;
use crate::utils::display_width;
use crate::widget::theme::{DARK_GRAY, DISABLED_FG, PLACEHOLDER_FG};
use crate::widget::{RenderContext, View, WidgetProps};
use crate::{impl_props_builders, impl_styled_view};
const DEFAULT_PEEK_TIMEOUT: usize = 10;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum MaskStyle {
#[default]
Full,
ShowLast(usize),
ShowFirst(usize),
Peek,
Hidden,
}
#[derive(Clone, Debug, PartialEq)]
pub enum ValidationState {
None,
Valid,
Invalid(String),
Validating,
}
#[derive(Clone, Debug)]
pub struct MaskedInput {
value: String,
mask_char: char,
mask_style: MaskStyle,
placeholder: Option<String>,
label: Option<String>,
max_length: usize,
min_length: usize,
cursor: usize,
focused: bool,
disabled: bool,
fg: Option<Color>,
bg: Option<Color>,
width: Option<u16>,
validation: ValidationState,
show_strength: bool,
allow_reveal: bool,
revealing: bool,
peek_timeout: usize,
peek_countdown: usize,
props: WidgetProps,
}
impl MaskedInput {
pub fn new() -> Self {
Self {
value: String::new(),
mask_char: '●',
mask_style: MaskStyle::Full,
placeholder: None,
label: None,
max_length: 0,
min_length: 0,
cursor: 0,
focused: false,
disabled: false,
fg: None,
bg: None,
width: None,
validation: ValidationState::None,
show_strength: false,
allow_reveal: false,
revealing: false,
peek_timeout: DEFAULT_PEEK_TIMEOUT,
peek_countdown: 0,
props: WidgetProps::new(),
}
}
pub fn password() -> Self {
Self::new()
.mask_char('●')
.mask_style(MaskStyle::Full)
.show_strength(true)
}
pub fn pin(length: usize) -> Self {
Self::new()
.mask_char('*')
.max_length(length)
.mask_style(MaskStyle::Full)
}
pub fn credit_card() -> Self {
Self::new()
.mask_char('•')
.mask_style(MaskStyle::ShowLast(4))
.max_length(16)
}
pub fn mask_char(mut self, c: char) -> Self {
self.mask_char = c;
self
}
pub fn mask_style(mut self, style: MaskStyle) -> Self {
self.mask_style = style;
self
}
pub fn placeholder(mut self, text: impl Into<String>) -> Self {
self.placeholder = Some(text.into());
self
}
pub fn label(mut self, text: impl Into<String>) -> Self {
self.label = Some(text.into());
self
}
pub fn max_length(mut self, len: usize) -> Self {
self.max_length = len;
self
}
pub fn min_length(mut self, len: usize) -> Self {
self.min_length = len;
self
}
pub fn focused(mut self, focused: bool) -> Self {
self.focused = focused;
self
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
pub fn fg(mut self, color: Color) -> Self {
self.fg = Some(color);
self
}
pub fn bg(mut self, color: Color) -> Self {
self.bg = Some(color);
self
}
pub fn width(mut self, width: u16) -> Self {
self.width = Some(width);
self
}
pub fn show_strength(mut self, show: bool) -> Self {
self.show_strength = show;
self
}
pub fn allow_reveal(mut self, allow: bool) -> Self {
self.allow_reveal = allow;
self
}
pub fn value(mut self, value: impl Into<String>) -> Self {
self.value = value.into();
self.cursor = self.value.len();
self
}
pub fn get_value(&self) -> &str {
&self.value
}
pub fn get_mask_char(&self) -> char {
self.mask_char
}
pub fn get_mask_style(&self) -> MaskStyle {
self.mask_style
}
pub fn get_placeholder(&self) -> Option<&String> {
self.placeholder.as_ref()
}
pub fn get_label(&self) -> Option<&String> {
self.label.as_ref()
}
pub fn get_max_length(&self) -> usize {
self.max_length
}
pub fn get_min_length(&self) -> usize {
self.min_length
}
pub fn get_cursor(&self) -> usize {
self.cursor
}
pub fn set_cursor(&mut self, pos: usize) {
self.cursor = pos.min(self.value.len());
}
pub fn get_focused(&self) -> bool {
self.focused
}
pub fn get_disabled(&self) -> bool {
self.disabled
}
pub fn get_fg(&self) -> Option<Color> {
self.fg
}
pub fn get_bg(&self) -> Option<Color> {
self.bg
}
pub fn get_width(&self) -> Option<u16> {
self.width
}
pub fn get_show_strength(&self) -> bool {
self.show_strength
}
pub fn get_allow_reveal(&self) -> bool {
self.allow_reveal
}
pub fn get_revealing(&self) -> bool {
self.revealing
}
pub fn set_revealing(&mut self, revealing: bool) {
self.revealing = revealing;
}
pub fn get_peek_countdown(&self) -> usize {
self.peek_countdown
}
pub fn set_peek_countdown(&mut self, countdown: usize) {
self.peek_countdown = countdown;
}
pub fn get_validation(&self) -> &ValidationState {
&self.validation
}
pub fn set_value(&mut self, value: impl Into<String>) {
self.value = value.into();
self.cursor = self.cursor.min(self.value.len());
}
pub fn clear(&mut self) {
self.value.clear();
self.cursor = 0;
}
pub fn toggle_reveal(&mut self) {
if self.allow_reveal {
self.revealing = !self.revealing;
}
}
pub fn insert_char(&mut self, c: char) {
if self.disabled {
return;
}
if self.max_length > 0 && self.value.len() >= self.max_length {
return;
}
self.value.insert(self.cursor, c);
self.cursor += 1;
if matches!(self.mask_style, MaskStyle::Peek) {
self.peek_countdown = self.peek_timeout;
}
}
pub fn delete_backward(&mut self) {
if self.disabled || self.cursor == 0 {
return;
}
self.cursor -= 1;
self.value.remove(self.cursor);
}
pub fn delete_forward(&mut self) {
if self.disabled || self.cursor >= self.value.len() {
return;
}
self.value.remove(self.cursor);
}
pub fn move_left(&mut self) {
if self.cursor > 0 {
self.cursor -= 1;
}
}
pub fn move_right(&mut self) {
if self.cursor < self.value.len() {
self.cursor += 1;
}
}
pub fn move_start(&mut self) {
self.cursor = 0;
}
pub fn move_end(&mut self) {
self.cursor = self.value.len();
}
pub fn update(&mut self) {
if self.peek_countdown > 0 {
self.peek_countdown -= 1;
}
}
pub fn password_strength(&self) -> usize {
let len = self.value.len();
let has_lower = self.value.chars().any(|c| c.is_lowercase());
let has_upper = self.value.chars().any(|c| c.is_uppercase());
let has_digit = self.value.chars().any(|c| c.is_ascii_digit());
let has_special = self.value.chars().any(|c| !c.is_alphanumeric());
let mut strength = 0;
if len >= 8 {
strength += 1;
}
if len >= 12 {
strength += 1;
}
if has_lower && has_upper {
strength += 1;
}
if has_digit {
strength += 1;
}
if has_special {
strength += 1;
}
strength.min(4)
}
pub fn strength_label(&self) -> &str {
match self.password_strength() {
0 => "Very Weak",
1 => "Weak",
2 => "Fair",
3 => "Strong",
_ => "Very Strong",
}
}
pub fn strength_color(&self) -> Color {
match self.password_strength() {
0 => Color::RED,
1 => Color::rgb(255, 128, 0), 2 => Color::YELLOW,
3 => Color::rgb(128, 255, 0), _ => Color::GREEN,
}
}
pub fn validate(&mut self) -> bool {
if self.min_length > 0 && self.value.len() < self.min_length {
self.validation = ValidationState::Invalid(format!(
"Minimum {} characters required",
self.min_length
));
return false;
}
self.validation = ValidationState::Valid;
true
}
pub fn masked_display(&self) -> String {
if self.revealing {
return self.value.clone();
}
let len = self.value.len();
if len == 0 {
return String::new();
}
match self.mask_style {
MaskStyle::Full => {
std::iter::repeat_n(self.mask_char, len).collect()
}
MaskStyle::ShowLast(n) => {
if len <= n {
self.value.clone()
} else {
let mask_count = len - n;
let mut result = String::with_capacity(len);
result.extend(std::iter::repeat_n(self.mask_char, mask_count));
result.push_str(&self.value[len - n..]);
result
}
}
MaskStyle::ShowFirst(n) => {
if len <= n {
self.value.clone()
} else {
let mut result = String::with_capacity(len);
result.push_str(&self.value[..n]);
result.extend(std::iter::repeat_n(self.mask_char, len - n));
result
}
}
MaskStyle::Peek => {
if self.peek_countdown > 0 && self.cursor > 0 && self.cursor <= len {
let last_char = self
.value
.char_indices()
.nth(self.cursor - 1)
.map(|(_, c)| c)
.unwrap_or(' ');
let mut result = String::with_capacity(len);
result.extend(std::iter::repeat_n(self.mask_char, self.cursor - 1));
result.push(last_char);
result.extend(std::iter::repeat_n(self.mask_char, len - self.cursor));
result
} else {
std::iter::repeat_n(self.mask_char, len).collect()
}
}
MaskStyle::Hidden => String::new(),
}
}
}
impl Default for MaskedInput {
fn default() -> Self {
Self::new()
}
}
impl View for MaskedInput {
crate::impl_view_meta!("MaskedInput");
fn render(&self, ctx: &mut RenderContext) {
use crate::widget::stack::{hstack, vstack};
use crate::widget::Text;
let mut content = vstack();
if let Some(label) = &self.label {
content = content.child(Text::new(label).bold());
}
let display = if self.value.is_empty() {
self.placeholder.clone().unwrap_or_default()
} else {
self.masked_display()
};
let is_placeholder = self.value.is_empty() && self.placeholder.is_some();
let width = self.width.unwrap_or(20) as usize;
let display_w = display_width(&display);
let padded = if display_w < width {
let mut result = String::with_capacity(width);
result.push_str(&display);
result.extend(std::iter::repeat_n(' ', width - display_w));
result
} else {
crate::utils::truncate_to_width(&display, width).to_owned()
};
let display_with_cursor = if self.focused && !self.disabled {
let cursor_pos = self.cursor.min(padded.chars().count());
let before: String = padded.chars().take(cursor_pos).collect();
let cursor_char = padded.chars().skip(cursor_pos).next().unwrap_or(' ');
let after: String = padded.chars().skip(cursor_pos + 1).collect();
(before, cursor_char, after)
} else {
(padded.clone(), ' ', String::new())
};
let mut input_text = if self.focused && !self.disabled {
hstack()
.child(Text::new(display_with_cursor.0))
.child(
Text::new(display_with_cursor.1.to_string())
.bg(Color::WHITE)
.fg(Color::BLACK),
)
.child(Text::new(display_with_cursor.2))
} else {
let mut text = Text::new(&padded);
if is_placeholder {
text = text.fg(PLACEHOLDER_FG);
} else if self.disabled {
text = text.fg(DISABLED_FG);
} else if let Some(fg) = self.fg {
text = text.fg(fg);
}
hstack().child(text)
};
if self.allow_reveal {
let eye = if self.revealing {
"👁"
} else {
"👁🗨"
};
input_text = input_text.child(Text::new(format!(" {}", eye)));
}
let border_color = if self.disabled {
DARK_GRAY
} else if matches!(self.validation, ValidationState::Invalid(_)) {
Color::RED
} else if matches!(self.validation, ValidationState::Valid) {
Color::GREEN
} else if self.focused {
Color::CYAN
} else {
PLACEHOLDER_FG
};
let bordered = hstack()
.child(Text::new("[").fg(border_color))
.child(input_text)
.child(Text::new("]").fg(border_color));
content = content.child(bordered);
if self.show_strength && !self.value.is_empty() {
let strength = self.password_strength();
let color = self.strength_color();
let bar: String = std::iter::repeat_n('█', strength + 1).collect();
let empty: String = std::iter::repeat_n('░', 4 - strength).collect();
let strength_display = hstack()
.child(Text::new(&bar).fg(color))
.child(Text::new(&empty).fg(DARK_GRAY))
.child(Text::new(format!(" {}", self.strength_label())).fg(color));
content = content.child(strength_display);
}
if let ValidationState::Invalid(msg) = &self.validation {
content = content.child(Text::new(msg).fg(Color::RED));
}
content.render(ctx);
}
}
impl_styled_view!(MaskedInput);
impl_props_builders!(MaskedInput);
impl MaskedInput {
pub fn set_id(&mut self, id: impl Into<String>) {
self.props.id = Some(id.into());
}
pub fn add_class(&mut self, class: impl Into<String>) {
let class_str = class.into();
if !self.props.classes.contains(&class_str) {
self.props.classes.push(class_str);
}
}
pub fn remove_class(&mut self, class: &str) {
self.props.classes.retain(|c| c != class);
}
pub fn toggle_class(&mut self, class: &str) {
if self.has_class(class) {
self.remove_class(class);
} else {
self.props.classes.push(class.to_string());
}
}
pub fn has_class(&self, class: &str) -> bool {
self.props.classes.iter().any(|c| c == class)
}
pub fn get_classes(&self) -> &[String] {
&self.props.classes
}
pub fn get_id(&self) -> Option<&str> {
self.props.id.as_deref()
}
}
pub fn masked_input() -> MaskedInput {
MaskedInput::new()
}
pub fn password_input(placeholder: impl Into<String>) -> MaskedInput {
MaskedInput::password().placeholder(placeholder)
}
pub fn pin_input(length: usize) -> MaskedInput {
MaskedInput::pin(length)
}
pub fn credit_card_input() -> MaskedInput {
MaskedInput::credit_card()
}