use super::{Widget, WidgetBase, WidgetId, LayoutContext, PaintContext, EventContext};
use crate::css::{ClassList, WidgetState};
use crate::event::{Event, EventResult, MouseEventKind, KeyEventKind, Key};
use crate::geometry::{BorderRadius, Point, Rect, Size};
use crate::layout::{Constraints, LayoutResult};
use crate::render::Painter;
#[allow(clippy::type_complexity)]
pub struct PasswordField {
base: WidgetBase,
value: String,
placeholder: String,
mask_char: char,
show_toggle: bool,
is_revealed: bool,
on_change: Option<Box<dyn Fn(&str) + Send + Sync>>,
on_submit: Option<Box<dyn Fn(&str) + Send + Sync>>,
cursor_position: usize,
cursor_visible: bool,
}
impl PasswordField {
pub fn new() -> Self {
Self {
base: WidgetBase::new().with_class("password-field"),
value: String::new(),
placeholder: String::new(),
mask_char: '•',
show_toggle: true,
is_revealed: false,
on_change: None,
on_submit: None,
cursor_position: 0,
cursor_visible: true,
}
}
pub fn placeholder(mut self, text: impl Into<String>) -> Self {
self.placeholder = text.into();
self
}
pub fn mask_char(mut self, c: char) -> Self {
self.mask_char = c;
self
}
pub fn show_toggle(mut self, show: bool) -> Self {
self.show_toggle = show;
self
}
pub fn on_change<F>(mut self, handler: F) -> Self
where
F: Fn(&str) + Send + Sync + 'static,
{
self.on_change = Some(Box::new(handler));
self
}
pub fn on_submit<F>(mut self, handler: F) -> Self
where
F: Fn(&str) + Send + Sync + 'static,
{
self.on_submit = Some(Box::new(handler));
self
}
pub fn class(mut self, class: &str) -> Self {
self.base.classes.add(class);
self
}
pub fn id(mut self, id: &str) -> Self {
self.base.element_id = Some(id.to_string());
self
}
pub fn value(&self) -> &str {
&self.value
}
pub fn set_value(&mut self, value: impl Into<String>) {
self.value = value.into();
self.cursor_position = self.value.len();
}
pub fn clear(&mut self) {
self.value.clear();
self.cursor_position = 0;
}
pub fn toggle_visibility(&mut self) {
self.is_revealed = !self.is_revealed;
}
fn display_text(&self) -> String {
if self.is_revealed {
self.value.clone()
} else {
self.mask_char.to_string().repeat(self.value.len())
}
}
fn handle_key_input(&mut self, key: &Key, text: Option<&str>) -> bool {
match key {
Key::Backspace => {
if self.cursor_position > 0 {
self.cursor_position -= 1;
self.value.remove(self.cursor_position);
if let Some(handler) = &self.on_change {
handler(&self.value);
}
return true;
}
}
Key::Delete => {
if self.cursor_position < self.value.len() {
self.value.remove(self.cursor_position);
if let Some(handler) = &self.on_change {
handler(&self.value);
}
return true;
}
}
Key::Left => {
if self.cursor_position > 0 {
self.cursor_position -= 1;
return true;
}
}
Key::Right => {
if self.cursor_position < self.value.len() {
self.cursor_position += 1;
return true;
}
}
Key::Home => {
self.cursor_position = 0;
return true;
}
Key::End => {
self.cursor_position = self.value.len();
return true;
}
Key::Enter => {
if let Some(handler) = &self.on_submit {
handler(&self.value);
}
return true;
}
_ => {}
}
if let Some(text) = text {
for c in text.chars() {
if !c.is_control() {
self.value.insert(self.cursor_position, c);
self.cursor_position += 1;
}
}
if let Some(handler) = &self.on_change {
handler(&self.value);
}
return true;
}
false
}
}
impl Default for PasswordField {
fn default() -> Self {
Self::new()
}
}
impl Widget for PasswordField {
fn id(&self) -> WidgetId {
self.base.id
}
fn type_name(&self) -> &'static str {
"password-field"
}
fn element_id(&self) -> Option<&str> {
self.base.element_id.as_deref()
}
fn classes(&self) -> &ClassList {
&self.base.classes
}
fn state(&self) -> WidgetState {
self.base.state
}
fn intrinsic_size(&self, _ctx: &LayoutContext) -> Size {
Size::new(200.0, 40.0)
}
fn layout(&mut self, constraints: Constraints, ctx: &LayoutContext) -> LayoutResult {
let intrinsic = self.intrinsic_size(ctx);
let size = Size::new(
constraints.max_width.min(intrinsic.width.max(constraints.min_width)),
constraints.constrain(intrinsic).height,
);
self.base.bounds.size = size;
LayoutResult::new(size)
}
fn paint(&self, painter: &mut Painter, rect: Rect, ctx: &PaintContext) {
let theme = ctx.style_ctx.theme;
let bg_color = if self.base.state.disabled {
theme.colors.muted
} else {
theme.colors.background
};
let radius = BorderRadius::all(theme.radii.md * theme.typography.base_size);
painter.fill_rounded_rect(rect, bg_color, radius);
let border_color = if self.base.state.focused {
theme.colors.ring
} else if self.base.state.hovered {
theme.colors.ring.with_alpha(0.5)
} else {
theme.colors.border
};
painter.stroke_rect(rect, border_color, 1.0);
let font_size = 14.0;
let padding = 12.0;
let text_x = rect.x() + padding;
let text_y = rect.y() + (rect.height() + font_size * 0.8) / 2.0;
let toggle_width = if self.show_toggle { 32.0 } else { 0.0 };
let _text_area_width = rect.width() - padding * 2.0 - toggle_width;
if self.value.is_empty() {
painter.draw_text(
&self.placeholder,
Point::new(text_x, text_y),
theme.colors.muted_foreground,
font_size,
);
} else {
let display = self.display_text();
painter.draw_text(
&display,
Point::new(text_x, text_y),
theme.colors.foreground,
font_size,
);
if self.base.state.focused && self.cursor_visible {
let cursor_text = if self.is_revealed {
&self.value[..self.cursor_position]
} else {
&self.mask_char.to_string().repeat(self.cursor_position)
};
let cursor_x = text_x + cursor_text.len() as f32 * font_size * 0.6;
painter.fill_rect(
Rect::new(cursor_x, rect.y() + 8.0, 2.0, rect.height() - 16.0),
theme.colors.foreground,
);
}
}
if self.show_toggle {
let toggle_x = rect.x() + rect.width() - toggle_width - 4.0;
let toggle_y = rect.y() + (rect.height() - 16.0) / 2.0;
let icon_color = if self.is_revealed {
theme.colors.primary
} else {
theme.colors.muted_foreground
};
painter.fill_rect(
Rect::new(toggle_x + 4.0, toggle_y + 6.0, 16.0, 4.0),
icon_color,
);
}
if self.base.state.focused && ctx.focus_visible {
let ring_rect = Rect::new(
rect.x() - 2.0,
rect.y() - 2.0,
rect.width() + 4.0,
rect.height() + 4.0,
);
painter.stroke_rect(ring_rect, theme.colors.ring, 2.0);
}
}
fn handle_event(&mut self, event: &Event, ctx: &mut EventContext) -> EventResult {
match event {
Event::Mouse(mouse) => {
let in_bounds = self.bounds().contains(mouse.position);
match mouse.kind {
MouseEventKind::Move | MouseEventKind::Enter => {
if in_bounds && !self.base.state.hovered {
self.base.state.hovered = true;
ctx.request_redraw();
} else if !in_bounds && self.base.state.hovered {
self.base.state.hovered = false;
ctx.request_redraw();
}
}
MouseEventKind::Leave => {
if self.base.state.hovered {
self.base.state.hovered = false;
ctx.request_redraw();
}
}
MouseEventKind::Down if in_bounds => {
self.base.state.focused = true;
ctx.request_focus(self.base.id);
ctx.request_redraw();
if self.show_toggle {
let toggle_x = self.base.bounds.x() + self.base.bounds.width() - 36.0;
if mouse.position.x >= toggle_x {
self.toggle_visibility();
}
}
return EventResult::Handled;
}
_ => {}
}
}
Event::Key(key) if self.base.state.focused => {
if key.kind == KeyEventKind::Down
&& self.handle_key_input(&key.key, key.text.as_deref()) {
ctx.request_redraw();
return EventResult::Handled;
}
}
_ => {}
}
EventResult::Ignored
}
fn bounds(&self) -> Rect {
self.base.bounds
}
fn set_bounds(&mut self, bounds: Rect) {
self.base.bounds = bounds;
}
}