Input
For multiple addons, shared frames, and textarea toolbars, see Input Group.
A single-line text input with validation, masking, prefix/suffix elements, and different visual states. Use Textarea for ordinary multi-line text and Editor for source code.
#Import
use gpui_kit::component::input::{Input, InputState};
#Usage
#Basic Input
let input = cx.new(|cx| InputState::new(window, cx));
Input::new(&input)
#With Placeholder
let input = cx.new(|cx|
InputState::new(window, cx)
.placeholder("Enter your name...")
);
Input::new(&input)
#With Default Value
let input = cx.new(|cx|
InputState::new(window, cx)
.default_value("John Doe")
);
Input::new(&input)
#Cleanable Input
Input::new(&input)
.cleanable(true) // Show clear button when input has value
#With Prefix and Suffix
use gpui_kit::component::{Icon, IconName};
// With prefix icon
Input::new(&input)
.prefix(Icon::new(IconName::Search).small())
// With suffix button
Input::new(&input)
.suffix(
Button::new("info")
.ghost()
.icon(IconName::Info)
.xsmall()
)
// With both
Input::new(&input)
.prefix(Icon::new(IconName::Search).small())
.suffix(Button::new("btn").ghost().icon(IconName::Info).xsmall())
#Password Input (Masked)
let input = cx.new(|cx|
InputState::new(window, cx)
.masked(true)
.default_value("password123")
);
Input::new(&input)
.content_type(InputContentType::Password)
.mask_toggle() // Shows toggle button to reveal password
While the value is masked, the input keeps it out of the clipboard and out of
the selection: Copy and Cut do nothing (and are disabled in the context menu),
a word-wise delete takes everything before the caret, and a double click
selects the whole value instead of one word. Paste and Select All keep working,
and revealing the value with mask_toggle restores all of them.
#Input Sizes
Input::new(&input).large()
Input::new(&input) // medium (default)
Input::new(&input).small()
#Disabled Input
Input::new(&input).disabled(true)
#Read-only Input
Unlike disabled, a read-only input keeps the normal appearance and still can
be focused, selected and copied, it only rejects the changes made by the user.
Input::new(&input).readonly(true)
#Clean on ESC
let input = cx.new(|cx|
InputState::new(window, cx)
.clean_on_escape() // Clear input when ESC is pressed
);
Input::new(&input)
#Input Validation
// Validate float numbers
let input = cx.new(|cx|
InputState::new(window, cx)
.validate(|s, _| s.parse::<f32>().is_ok())
);
// Regex pattern validation
let input = cx.new(|cx|
InputState::new(window, cx)
.pattern(regex::Regex::new(r"^[a-zA-Z0-9]*$").unwrap())
);
#Input Masking
// Phone number mask
let input = cx.new(|cx|
InputState::new(window, cx)
.mask_pattern("(999)-999-9999")
);
// Custom pattern: AAA-###-AAA (A=letter, #=digit, 9=digit optional)
let input = cx.new(|cx|
InputState::new(window, cx)
.mask_pattern("AAA-###-AAA")
);
// Number with thousands separator
use gpui_kit::component::input::MaskPattern;
let input = cx.new(|cx|
InputState::new(window, cx)
.mask_pattern(MaskPattern::Number {
separator: Some(','),
fraction: Some(3),
})
);
#Handle Input Events
let input = cx.new(|cx| InputState::new(window, cx));
cx.subscribe_in(&input, window, |view, state, event, window, cx| {
match event {
InputEvent::Change => {
let text = state.read(cx).value();
println!("Input changed: {}", text);
}
InputEvent::PressEnter { secondary } => {
println!("Enter pressed, secondary: {}", secondary);
}
InputEvent::Focus => println!("Input focused"),
InputEvent::Blur => println!("Input blurred"),
}
});
#Custom Appearance
// Without default styling
Input::new(&input).appearance(false)
// Use in custom container
div()
.border_b_2()
.px_6()
.py_3()
.border_color(cx.theme().border)
.bg(cx.theme().secondary)
.child(Input::new(&input).appearance(false))
#Context Menu
// The built-in context menu can be disabled.
let input = cx.new(|cx| InputState::new(window, cx).context_menu(false));
// Or you can define a custom context menu.
Input::new(&input).context_menu(|menu, window, cx| {
// You can define your own actions and even utilize
// built-in actions (cut, copy, paste, etc.)
// to avoid having to re-implement that functionality.
menu.menu("Custom Action", Box::new(CustomAction))
.separator()
.menu("Cut", Box::new(input::Cut))
.menu("Copy", Box::new(input::Copy))
.menu("Paste", Box::new(input::Paste))
})
#Touch Selection
On a touch screen, a long press selects the word under the finger and keeps
following the finger while it stays down. Lifting it opens an edit menu over
the selection with the commands that apply — Cut, Copy, Paste, and
Select All — and puts a grab handle at each end of the selection. Dragging a
handle moves that end; the other end stays put, and a multi-line input scrolls
when the finger reaches its edge. A long press on whitespace or in an empty
field places the caret and offers Paste and Select All.
The handles and the menu belong to the selection the gesture made. They
disappear as soon as anything else moves the selection — a tap, typing, an
arrow key, Escape — and the menu steps aside while the content scrolls under
a finger. Tapping the selected text brings the menu back.
Cut, Copy, and Paste go through the input’s own actions, so a custom key
binding or an open completion menu sees them the same way. A read-only input
offers only Copy and Select All; a masked input keeps its value out of the
clipboard.
#Paste Hook
on_paste intercepts the clipboard before the default text insertion, so
pasted images and copied files can live in app-owned state instead of being
silently dropped. It is available on Input, Textarea and Editor.
use gpui_kit::ClipboardEntry;
let view = cx.entity().downgrade();
Textarea::new(&self.composer).on_paste(move |item, _, cx| {
let images: Vec<_> = item.entries().iter().filter_map(|entry| match entry {
ClipboardEntry::Image(image) => Some(image.clone()),
_ => None,
}).collect();
if images.is_empty() {
return false; // fall through to the default text insertion
}
view.update(cx, |this, cx| {
// Store the images beside the input, e.g. as `Attachment`s.
this.attachments.extend(images);
cx.notify();
}).ok();
true // consumed, the input inserts nothing
})
Return true when the handler took the paste: the input::Paste action
stops there and the input inserts nothing. Return false to let the action
reach the engine, which inserts clipboard.text() as before. Copied files
arrive as ClipboardEntry::ExternalPaths through the same hook.
Known limit: on web read_from_clipboard() is None (text arrives through
the platform input handler); image paste there needs async clipboard access
and permission, and is out of scope.
#Examples
#Search Input
let search = cx.new(|cx|
InputState::new(window, cx)
.placeholder("Search...")
);
Input::new(&search)
.prefix(Icon::new(IconName::Search).small())
#Currency Input
let amount = cx.new(|cx|
InputState::new(window, cx)
.mask_pattern(MaskPattern::Number {
separator: Some(','),
fraction: Some(2),
})
);
div()
.child(Input::new(&amount))
.child(format!("Value: {}", amount.read(cx).value()))
#Form with Multiple Inputs
struct FormView {
name_input: Entity<InputState>,
email_input: Entity<InputState>,
}
v_flex()
.gap_3()
.child(Input::new(&self.name_input))
.child(Input::new(&self.email_input))