use gpui::*;
use gpui::prelude::FluentBuilder;
use std::sync::Arc;
#[derive(Clone, Debug)]
pub enum TextInputEvent {
Change(String),
Submit(String),
Focus,
Blur,
}
pub struct TextInput {
value: String,
placeholder: String,
focus_handle: FocusHandle,
disabled: bool,
is_password: bool,
max_length: Option<usize>,
validator: Option<Arc<dyn Fn(&str) -> bool>>,
}
impl TextInput {
pub fn new(cx: &mut Context<Self>) -> Self {
Self {
value: String::new(),
placeholder: String::new(),
focus_handle: cx.focus_handle(),
disabled: false,
is_password: false,
max_length: None,
validator: None,
}
}
pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
self.placeholder = placeholder.into();
self
}
pub fn value(mut self, value: impl Into<String>) -> Self {
self.value = value.into();
self
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
pub fn password(mut self, is_password: bool) -> Self {
self.is_password = is_password;
self
}
pub fn max_length(mut self, max_length: usize) -> Self {
self.max_length = Some(max_length);
self
}
pub fn validator<F>(mut self, validator: F) -> Self
where
F: Fn(&str) -> bool + 'static,
{
self.validator = Some(Arc::new(validator));
self
}
pub fn get_value(&self) -> &str {
&self.value
}
pub fn set_value(&mut self, value: String, cx: &mut Context<Self>) {
if let Some(ref validator) = self.validator {
if !validator(&value) {
return;
}
}
if let Some(max_len) = self.max_length {
if value.len() > max_len {
return;
}
}
self.value = value.clone();
cx.emit(TextInputEvent::Change(value));
cx.notify();
}
pub fn clear(&mut self, cx: &mut Context<Self>) {
self.value.clear();
cx.emit(TextInputEvent::Change(String::new()));
cx.notify();
}
pub fn focus(&self, window: &mut Window) {
self.focus_handle.focus(window);
}
fn handle_input(&mut self, text: &str, cx: &mut Context<Self>) {
if self.disabled {
return;
}
let mut new_value = self.value.clone();
new_value.push_str(text);
if let Some(max_len) = self.max_length {
if new_value.len() > max_len {
return;
}
}
if let Some(ref validator) = self.validator {
if !validator(&new_value) {
return;
}
}
self.value = new_value.clone();
cx.emit(TextInputEvent::Change(new_value));
cx.notify();
}
fn handle_backspace(&mut self, cx: &mut Context<Self>) {
if self.disabled || self.value.is_empty() {
return;
}
self.value.pop();
cx.emit(TextInputEvent::Change(self.value.clone()));
cx.notify();
}
fn handle_submit(&mut self, cx: &mut Context<Self>) {
if self.disabled {
return;
}
cx.emit(TextInputEvent::Submit(self.value.clone()));
}
fn render_display_text(&self) -> String {
if self.is_password && !self.value.is_empty() {
"•".repeat(self.value.len())
} else {
self.value.clone()
}
}
}
impl EventEmitter<TextInputEvent> for TextInput {}
impl Focusable for TextInput {
fn focus_handle(&self, _cx: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl Render for TextInput {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let is_focused = self.focus_handle.is_focused(window);
let display_text = self.render_display_text();
let show_placeholder = self.value.is_empty();
let disabled = self.disabled;
let placeholder = self.placeholder.clone();
div()
.id("text-input")
.track_focus(&self.focus_handle)
.on_key_down(cx.listener(|this, event: &KeyDownEvent, _, cx| {
if this.disabled {
return;
}
match event.keystroke.key.as_str() {
"backspace" => {
this.handle_backspace(cx);
}
"enter" => {
this.handle_submit(cx);
}
_ => {
if let Some(ch) = &event.keystroke.key_char {
this.handle_input(ch, cx);
}
}
}
}))
.on_mouse_down(MouseButton::Left, cx.listener(|_, _, window, cx| {
cx.emit(TextInputEvent::Focus);
cx.focus_self(window);
}))
.flex()
.items_center()
.w_full()
.h(px(36.))
.px_3()
.bg(if disabled {
rgb(0xF5F5F5)
} else {
rgb(0xFFFFFF)
})
.border_1()
.border_color(if is_focused {
rgb(0x696FC7)
} else {
rgb(0xE0E0E0)
})
.rounded(px(6.))
.when(!disabled, |this| {
this.cursor(CursorStyle::IBeam)
})
.child(
div()
.flex_1()
.text_sm()
.when(show_placeholder, |this| {
this.text_color(rgb(0x999999))
.child(placeholder)
})
.when(!show_placeholder, |this| {
this.text_color(if disabled {
rgb(0x999999)
} else {
rgb(0x333333)
})
.child(display_text.clone())
})
)
.when(is_focused && !disabled, |this| {
this.child(
div()
.w(px(1.))
.h(px(18.))
.bg(rgb(0x333333))
)
})
}
}