Files
gpui-native-kit/crates/core/src/input.rs
T
2026-11-07 17:00:36 +07:00

2584 lines
89 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use std::cell::RefCell;
use std::ops::Range;
use std::rc::Rc;
use std::time::{Duration, Instant};
use unicode_segmentation::UnicodeSegmentation;
use gpui::prelude::FluentBuilder as _;
use gpui::{
AnyElement, App, AppContext as _, Bounds, ClipboardItem, ContentMask, Context, CursorStyle,
DefiniteLength, DispatchPhase, ElementId, Entity, EntityInputHandler, EventEmitter, Font,
HitboxBehavior, InteractiveElement as _, IntoElement, KeyBinding, KeyDownEvent, MouseButton,
MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, RenderOnce, Role,
ScrollDelta, ScrollEvent, SharedString, StatefulInteractiveElement as _, Styled as _,
Subscription, Task, TextAlign, TextRun, UTF16Selection, Window, WrappedLine, actions, canvas,
div, fill, point, px,
};
use gpui_native_icons::IconRole;
use gpui_native_theme::{ActiveTheme as _, BORDER_WIDTH, ColorTokens, Size};
use gpui_native_utils::focus_ring;
use crate::button::{Button, ButtonVariant};
use crate::common::{Sizable, h_flex};
/// The key context input bindings match against.
pub const INPUT_CONTEXT: &str = "Input";
/// Consecutive edits within this interval merge into one undo entry.
const DEFAULT_GROUP_INTERVAL: Duration = Duration::from_millis(300);
/// How often the focused caret blinks.
const CARET_BLINK_INTERVAL: Duration = Duration::from_millis(500);
/// Pixel height of one scroll "line" for wheel scrolling.
const SCROLL_LINES_HEIGHT: f32 = 20.0;
/// The row count a multiline field shows until the app sets one.
const DEFAULT_ROWS: usize = 4;
/// Maximum undo entries kept; the oldest drops off the front.
const MAX_HISTORY: usize = 100;
/// The character a password field paints instead of each grapheme.
const MASK: &str = "\u{2022}";
/// The byte length of [`MASK`]; the masked display is `graphemes × MASK_LEN`.
const MASK_LEN: usize = 3;
/// Handler for key down events.
type KeyDownHandler = std::rc::Rc<dyn Fn(&KeyDownEvent, &mut Window, &mut App)>;
/// Handler for mouse down events.
type MouseDownHandler = std::rc::Rc<dyn Fn(&MouseDownEvent, &mut Window, &mut App)>;
/// Significant changes in an [`InputState`], observable via `cx.subscribe`.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum InputStateEvent {
/// The text content changed: typing, paste, IME commit, or `set_value`.
TextChanged,
/// The field gained focus.
Focused,
/// The field lost focus.
Blurred,
/// Enter was pressed
Submitted,
}
/// Which keystroke submits a multiline field.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum SubmitOn {
/// Enter inserts a newline; modifier + Enter submits.
#[default]
ModifiedEnter,
/// Enter submits; Shift + Enter inserts a newline.
Enter,
}
actions!(
input,
[
Escape,
Submit,
SecondarySubmit,
InsertNewline,
Up,
Down,
SelectUp,
SelectDown,
Backspace,
Delete,
DeleteWordLeft,
DeleteWordRight,
DeleteToBeginningOfLine,
Left,
Right,
SelectLeft,
SelectRight,
WordLeft,
WordRight,
SelectWordLeft,
SelectWordRight,
Home,
End,
SelectHome,
SelectEnd,
SelectAll,
Copy,
Cut,
Paste,
Undo,
Redo,
]
);
/// Keybindings for input actions.
#[derive(Clone, Debug)]
pub struct InputBindings {
pub escape: Option<KeyBinding>,
pub submit: Option<KeyBinding>,
pub secondary_submit: Option<KeyBinding>,
pub insert_newline: Option<KeyBinding>,
pub backspace: Option<KeyBinding>,
pub delete: Option<KeyBinding>,
pub delete_word_left: Option<KeyBinding>,
pub delete_word_right: Option<KeyBinding>,
pub delete_to_beginning_of_line: Option<KeyBinding>,
pub left: Option<KeyBinding>,
pub right: Option<KeyBinding>,
pub up: Option<KeyBinding>,
pub down: Option<KeyBinding>,
pub select_left: Option<KeyBinding>,
pub select_right: Option<KeyBinding>,
pub select_up: Option<KeyBinding>,
pub select_down: Option<KeyBinding>,
pub word_left: Option<KeyBinding>,
pub word_right: Option<KeyBinding>,
pub select_word_left: Option<KeyBinding>,
pub select_word_right: Option<KeyBinding>,
pub home: Option<KeyBinding>,
pub end: Option<KeyBinding>,
pub select_home: Option<KeyBinding>,
pub select_end: Option<KeyBinding>,
pub select_all: Option<KeyBinding>,
pub copy: Option<KeyBinding>,
pub cut: Option<KeyBinding>,
pub paste: Option<KeyBinding>,
pub undo: Option<KeyBinding>,
pub redo: Option<KeyBinding>,
}
impl Default for InputBindings {
/// The platform-specific defaults: cmd on macOS, ctrl elsewhere.
fn default() -> Self {
let context = Some(INPUT_CONTEXT);
#[cfg(target_os = "macos")]
let bindings = Self {
escape: Some(KeyBinding::new("escape", Escape, context)),
submit: Some(KeyBinding::new("enter", Submit, context)),
secondary_submit: Some(KeyBinding::new("cmd-enter", SecondarySubmit, context)),
insert_newline: Some(KeyBinding::new("shift-enter", InsertNewline, context)),
backspace: Some(KeyBinding::new("backspace", Backspace, context)),
delete: Some(KeyBinding::new("delete", Delete, context)),
delete_word_left: Some(KeyBinding::new("alt-backspace", DeleteWordLeft, context)),
delete_word_right: Some(KeyBinding::new("alt-delete", DeleteWordRight, context)),
delete_to_beginning_of_line: Some(KeyBinding::new(
"cmd-backspace",
DeleteToBeginningOfLine,
context,
)),
left: Some(KeyBinding::new("left", Left, context)),
right: Some(KeyBinding::new("right", Right, context)),
up: Some(KeyBinding::new("up", Up, context)),
down: Some(KeyBinding::new("down", Down, context)),
select_left: Some(KeyBinding::new("shift-left", SelectLeft, context)),
select_right: Some(KeyBinding::new("shift-right", SelectRight, context)),
select_up: Some(KeyBinding::new("shift-up", SelectUp, context)),
select_down: Some(KeyBinding::new("shift-down", SelectDown, context)),
word_left: Some(KeyBinding::new("alt-left", WordLeft, context)),
word_right: Some(KeyBinding::new("alt-right", WordRight, context)),
select_word_left: Some(KeyBinding::new("alt-shift-left", SelectWordLeft, context)),
select_word_right: Some(KeyBinding::new("alt-shift-right", SelectWordRight, context)),
home: Some(KeyBinding::new("home", Home, context)),
end: Some(KeyBinding::new("end", End, context)),
select_home: Some(KeyBinding::new("shift-home", SelectHome, context)),
select_end: Some(KeyBinding::new("shift-end", SelectEnd, context)),
select_all: Some(KeyBinding::new("cmd-a", SelectAll, context)),
copy: Some(KeyBinding::new("cmd-c", Copy, context)),
cut: Some(KeyBinding::new("cmd-x", Cut, context)),
paste: Some(KeyBinding::new("cmd-v", Paste, context)),
undo: Some(KeyBinding::new("cmd-z", Undo, context)),
redo: Some(KeyBinding::new("cmd-shift-z", Redo, context)),
};
#[cfg(not(target_os = "macos"))]
let bindings = Self {
escape: Some(KeyBinding::new("escape", Escape, context)),
submit: Some(KeyBinding::new("enter", Submit, context)),
secondary_submit: Some(KeyBinding::new("ctrl-enter", SecondarySubmit, context)),
insert_newline: Some(KeyBinding::new("shift-enter", InsertNewline, context)),
backspace: Some(KeyBinding::new("backspace", Backspace, context)),
delete: Some(KeyBinding::new("delete", Delete, context)),
delete_word_left: Some(KeyBinding::new("ctrl-backspace", DeleteWordLeft, context)),
delete_word_right: Some(KeyBinding::new("ctrl-delete", DeleteWordRight, context)),
delete_to_beginning_of_line: Some(KeyBinding::new(
"ctrl-shift-backspace",
DeleteToBeginningOfLine,
context,
)),
left: Some(KeyBinding::new("left", Left, context)),
right: Some(KeyBinding::new("right", Right, context)),
up: Some(KeyBinding::new("up", Up, context)),
down: Some(KeyBinding::new("down", Down, context)),
select_left: Some(KeyBinding::new("shift-left", SelectLeft, context)),
select_right: Some(KeyBinding::new("shift-right", SelectRight, context)),
select_up: Some(KeyBinding::new("shift-up", SelectUp, context)),
select_down: Some(KeyBinding::new("shift-down", SelectDown, context)),
word_left: Some(KeyBinding::new("ctrl-left", WordLeft, context)),
word_right: Some(KeyBinding::new("ctrl-right", WordRight, context)),
select_word_left: Some(KeyBinding::new("ctrl-shift-left", SelectWordLeft, context)),
select_word_right: Some(KeyBinding::new(
"ctrl-shift-right",
SelectWordRight,
context,
)),
home: Some(KeyBinding::new("home", Home, context)),
end: Some(KeyBinding::new("end", End, context)),
select_home: Some(KeyBinding::new("shift-home", SelectHome, context)),
select_end: Some(KeyBinding::new("shift-end", SelectEnd, context)),
select_all: Some(KeyBinding::new("ctrl-a", SelectAll, context)),
copy: Some(KeyBinding::new("ctrl-c", Copy, context)),
cut: Some(KeyBinding::new("ctrl-x", Cut, context)),
paste: Some(KeyBinding::new("ctrl-v", Paste, context)),
undo: Some(KeyBinding::new("ctrl-z", Undo, context)),
redo: Some(KeyBinding::new("ctrl-shift-z", Redo, context)),
};
bindings
}
}
impl InputBindings {
/// A table with every key unbound, for building custom bindings.
pub fn empty() -> Self {
Self {
escape: None,
submit: None,
secondary_submit: None,
insert_newline: None,
backspace: None,
delete: None,
delete_word_left: None,
delete_word_right: None,
delete_to_beginning_of_line: None,
left: None,
right: None,
up: None,
down: None,
select_left: None,
select_right: None,
select_up: None,
select_down: None,
word_left: None,
word_right: None,
select_word_left: None,
select_word_right: None,
home: None,
end: None,
select_home: None,
select_end: None,
select_all: None,
copy: None,
cut: None,
paste: None,
undo: None,
redo: None,
}
}
/// Collects the table into keybindings.
pub fn into_bindings(self) -> Vec<KeyBinding> {
let mut bindings: Vec<KeyBinding> = [
self.escape,
self.submit,
self.secondary_submit,
self.insert_newline,
self.backspace,
self.delete,
self.delete_word_left,
self.delete_word_right,
self.delete_to_beginning_of_line,
self.left,
self.right,
self.up,
self.down,
self.select_left,
self.select_right,
self.select_up,
self.select_down,
self.word_left,
self.word_right,
self.select_word_left,
self.select_word_right,
self.home,
self.end,
self.select_home,
self.select_end,
self.select_all,
self.copy,
self.cut,
self.paste,
self.undo,
self.redo,
]
.into_iter()
.flatten()
.collect();
#[cfg(target_os = "macos")]
{
let context = Some(INPUT_CONTEXT);
bindings.extend([
KeyBinding::new("cmd-left", Home, context),
KeyBinding::new("cmd-right", End, context),
KeyBinding::new("cmd-up", Home, context),
KeyBinding::new("cmd-down", End, context),
KeyBinding::new("cmd-shift-left", SelectHome, context),
KeyBinding::new("cmd-shift-right", SelectEnd, context),
]);
}
bindings
}
}
/// The shaped text: one wrapped line per buffer line, plus the mask map when masking.
struct ShapedText {
lines: Vec<ShapedLineEntry>,
mask_map: Option<Vec<(usize, usize)>>,
}
/// One shaped source line and its first byte's offset in the display text.
struct ShapedLineEntry {
start: usize,
line: WrappedLine,
}
/// The field's inner padding.
#[derive(Clone, Copy)]
struct FieldPadding {
left: Pixels,
right: Pixels,
/// Top inset; multiline fields balance it below the last row.
top: Pixels,
}
/// One visual (wrapped) row: a display-byte range and its y offset from the text origin.
struct VisualRow {
range: Range<usize>,
y: Pixels,
/// Index of the source line this row belongs to.
line_ix: usize,
}
/// Input layout information for hit testing and caret positioning.
struct InputLayout {
lines: Vec<ShapedLineEntry>,
/// The visual rows in order, top to bottom.
rows: Vec<VisualRow>,
bounds: Bounds<Pixels>,
/// Where the text origin (top-left of row 0) sits inside `bounds`.
origin: gpui::Point<Pixels>,
/// Maps display offsets to real buffer offsets for masked input.
mask_map: Option<Vec<(usize, usize)>>,
/// The height of one visual row.
line_height: Pixels,
/// Inner width available to the text.
text_width: Pixels,
/// Height of all rows stacked.
content_height: Pixels,
/// Height of the region the rows scroll within.
visible_height: Pixels,
}
impl InputLayout {
/// The x of a display index relative to its row's start.
fn row_x(&self, row_ix: usize, display: usize) -> Pixels {
let row = &self.rows[row_ix];
let entry = &self.lines[row.line_ix];
let layout = &entry.line.unwrapped_layout;
let row_start = row.range.start - entry.start;
layout.x_for_index(display - entry.start) - layout.x_for_index(row_start)
}
/// The row containing `display` and the caret's x relative to the row start.
fn row_for_index(&self, display: usize) -> Option<(usize, Pixels)> {
if self.rows.is_empty() {
return None;
}
let row_ix = row_ix_for_display(&self.rows, display);
let display = display.min(self.rows[row_ix].range.end);
Some((row_ix, self.row_x(row_ix, display)))
}
/// The display index within `row_ix` closest to `x`, which is relative to the row start.
fn index_at_row_x(&self, row_ix: usize, x: Pixels) -> usize {
let row = &self.rows[row_ix];
let entry = &self.lines[row.line_ix];
let layout = &entry.line.unwrapped_layout;
let row_start = row.range.start - entry.start;
layout
.closest_index_for_x(layout.x_for_index(row_start) + x)
.clamp(row_start, row.range.end - entry.start)
+ entry.start
}
/// The display index at a window point, using the last painted layout.
fn index_at(&self, position: gpui::Point<Pixels>) -> usize {
if self.rows.is_empty() {
return 0;
}
let rel_y = (position.y - self.origin.y) / self.line_height;
let row_ix = (rel_y.floor().max(0.0) as usize).min(self.rows.len() - 1);
self.index_at_row_x(row_ix, position.x - self.origin.x)
}
/// The display range of the row containing `display`.
fn row_range_at(&self, display: usize) -> Option<Range<usize>> {
self.rows
.iter()
.find(|row| display <= row.range.end)
.map(|row| row.range.clone())
}
/// The overlaps of a display range with each visual row:
/// `(row index, clipped range, the range continues past the row)`.
///
/// A range that strictly passes an empty row paints that row full width.
fn row_overlaps(&self, range: Range<usize>) -> Vec<(usize, Range<usize>, bool)> {
let mut overlaps = Vec::new();
for (row_ix, row) in self.rows.iter().enumerate() {
let touches = range.start < row.range.end
|| (row.range.is_empty() && range.start <= row.range.start);
let beyond = touches && range.end > row.range.end;
let start = range.start.clamp(row.range.start, row.range.end);
let end = range.end.clamp(row.range.start, row.range.end);
if start < end || beyond {
overlaps.push((row_ix, start..end, beyond));
}
}
overlaps
}
}
/// History entry for undo/redo operations.
#[derive(Clone, Debug)]
struct HistoryEntry {
/// The range the replacement occupies after the edit.
range: Range<usize>,
/// The text the edit replaced, restored on undo.
old_text: String,
/// The length of the replacement.
new_len: usize,
/// The selection before the edit.
cursor: usize,
anchor: usize,
/// When the edit happened, for grouping.
timestamp: Instant,
}
impl HistoryEntry {
/// Applies this entry in reverse, returning the entry that reverses it back.
fn reverse(&self, text: &mut String) -> HistoryEntry {
let end = (self.range.start + self.new_len).min(text.len());
let removed = text[self.range.start..end].to_string();
text.replace_range(self.range.start..end, &self.old_text);
HistoryEntry {
range: self.range.start..self.range.start + self.old_text.len(),
old_text: removed,
new_len: self.old_text.len(),
cursor: self.cursor,
anchor: self.anchor,
timestamp: self.timestamp,
}
}
}
/// Caret blink controller.
struct CaretBlink {
interval: Duration,
generation: usize,
visible: bool,
active: bool,
paused: bool,
/// Pending blink timers; held so they die with the controller.
tasks: Vec<Task<()>>,
}
impl CaretBlink {
fn new(_cx: &mut Context<Self>) -> Self {
Self {
interval: CARET_BLINK_INTERVAL,
generation: 0,
visible: true,
active: false,
paused: false,
tasks: Vec::new(),
}
}
fn visible(&self) -> bool {
self.visible
}
/// Starts blinking; the caret shows first and hides after one interval.
fn enable(&mut self, cx: &mut Context<Self>) {
if self.active {
return;
}
self.active = true;
self.visible = true;
self.paused = false;
self.schedule(cx);
}
/// Stops blinking. The caret stays hidden; paint gates on focus anyway.
fn disable(&mut self, cx: &mut Context<Self>) {
self.active = false;
self.visible = false;
self.paused = false;
cx.notify();
}
/// Holds the caret visible and restarts the hide timer.
fn pause(&mut self, cx: &mut Context<Self>) {
if !self.active {
return;
}
if !self.visible {
self.visible = true;
cx.notify();
}
self.paused = true;
self.generation += 1;
self.schedule_pause(cx);
}
fn tick(&mut self, cx: &mut Context<Self>) {
if !self.active || self.paused {
return;
}
self.visible = !self.visible;
cx.notify();
self.schedule(cx);
}
fn schedule(&mut self, cx: &mut Context<Self>) {
self.generation += 1;
self.schedule_pause(cx);
}
fn schedule_pause(&mut self, cx: &mut Context<Self>) {
let generation = self.generation;
let interval = self.interval;
let task = cx.spawn(async move |this, cx| {
cx.background_executor().timer(interval).await;
this.update(cx, |this, cx| {
if this.generation == generation {
this.paused = false;
this.tick(cx);
}
})
.ok();
});
self.tasks.push(task);
}
}
/// The application-owned state of a text field.
pub struct InputState {
text: String,
/// The caret's byte offset.
cursor: usize,
/// The byte offset the selection extends from.
anchor: usize,
/// The byte range of in-progress IME composition, if any.
marked_range: Option<Range<usize>>,
/// Whether the field had focus at the last paint, for focus transitions.
focused: bool,
focus_handle: gpui::FocusHandle,
/// The key-down handler, bound to this entity with `cx.listener`.
key_down_handler: Option<KeyDownHandler>,
/// The mouse-down handler, bound to this entity with `cx.listener`.
mouse_down_handler: Option<MouseDownHandler>,
/// Whether a drag-select is in progress.
dragging: bool,
/// Patch history. `redo_stack` holds undone entries awaiting redo.
undo_stack: Vec<HistoryEntry>,
redo_stack: Vec<HistoryEntry>,
/// Consecutive edits within this interval merge into one undo entry.
group_interval: Duration,
/// When set, user edits are refused, focus, movement, selection and copy stay.
read_only: bool,
/// When set, the field is a password field.
password: bool,
/// Whether a password field currently shows its content.
revealed: bool,
/// Whether the field holds multiple lines of text.
multiline: bool,
/// Which keystroke submits a multiline field.
submit_on: SubmitOn,
/// The x the caret keeps across vertical movement, reset on any other move.
goal_x: Option<Pixels>,
/// Content scroll inside the field: x for single-line, y for multiline, clamped at paint.
scroll_offset: gpui::Point<Pixels>,
/// Set by every caret-affecting change.
scroll_to_caret_pending: bool,
/// Drives the focused caret's blink.
caret_blink: Entity<CaretBlink>,
/// The shaped line and element bounds, recorded during each paint,
/// and shared with hit-testing and the IME geometry conversions.
layout: Rc<RefCell<Option<InputLayout>>>,
/// Keeps the field repainting on blink ticks. Dropping it unsubscribes.
_caret_blink_subscription: Subscription,
}
impl EventEmitter<InputStateEvent> for InputState {}
impl InputState {
fn new_inner(cx: &mut Context<Self>) -> Self {
let caret_blink = cx.new(CaretBlink::new);
Self {
text: String::new(),
cursor: 0,
anchor: 0,
marked_range: None,
focused: false,
focus_handle: cx.focus_handle(),
key_down_handler: Some(Rc::new(cx.listener(
|state, event: &KeyDownEvent, window, cx| {
state.handle_key_down(event, window, cx);
},
))),
mouse_down_handler: Some(Rc::new(cx.listener(
|state, event: &MouseDownEvent, window, cx| {
state.handle_mouse_down(event, window, cx);
},
))),
dragging: false,
undo_stack: Vec::new(),
redo_stack: Vec::new(),
group_interval: DEFAULT_GROUP_INTERVAL,
read_only: false,
password: false,
revealed: false,
multiline: false,
submit_on: SubmitOn::default(),
goal_x: None,
scroll_offset: point(Pixels::ZERO, Pixels::ZERO),
scroll_to_caret_pending: true,
caret_blink: caret_blink.clone(),
layout: Rc::new(RefCell::new(None)),
_caret_blink_subscription: cx.observe(&caret_blink, |_, _, cx| cx.notify()),
}
}
/// Creates the state entity for an application-owned input.
pub fn new(cx: &mut App) -> Entity<Self> {
cx.new(Self::new_inner)
}
/// Creates the state entity with initial text, caret at the end.
///
/// Newlines become spaces; flip the field to multiline with [`set_multiline`]
/// before `set_value` to seed lines.
pub fn new_with_text(text: impl Into<String>, cx: &mut App) -> Entity<Self> {
let text = sanitize(text.into(), false);
cx.new(|cx| {
let mut state = Self::new_inner(cx);
state.text = text;
state.cursor = state.text.len();
state.anchor = state.cursor;
state
})
}
/// The current text.
pub fn value(&self) -> &str {
&self.text
}
/// Replaces the whole value and collapses the selection to the end.
///
/// Newlines become spaces unless the field is multiline. Emits
/// [`InputStateEvent::TextChanged`] when the value changed; setting the
/// current value is a no-op.
pub fn set_value<V>(&mut self, value: V, window: &mut Window, cx: &mut Context<Self>)
where
V: Into<String>,
{
self.set_value_inner(value, true, window, cx);
}
/// Like [`set_value`], but does not emit `TextChanged`.
pub fn set_value_silent<V>(&mut self, value: V, window: &mut Window, cx: &mut Context<Self>)
where
V: Into<String>,
{
self.set_value_inner(value, false, window, cx);
}
fn set_value_inner<V>(
&mut self,
value: V,
emit: bool,
window: &mut Window,
cx: &mut Context<Self>,
) where
V: Into<String>,
{
let text = sanitize(value.into(), self.multiline);
if text == self.text {
return;
}
self.text = text;
self.cursor = self.text.len();
self.anchor = self.cursor;
self.marked_range = None;
self.undo_stack.clear();
self.redo_stack.clear();
if emit {
cx.emit(InputStateEvent::TextChanged);
}
self.notify(window, cx);
}
/// Sets how long consecutive edits keep merging into one undo entry.
pub fn set_group_interval(&mut self, interval: Duration) {
self.group_interval = interval;
}
/// Sets whether user edits are refused.
pub fn set_read_only(&mut self, read_only: bool, cx: &mut Context<Self>) {
if self.read_only != read_only {
self.read_only = read_only;
cx.notify();
}
}
/// Whether user edits are refused.
pub fn is_read_only(&self) -> bool {
self.read_only
}
/// Sets whether the field is a password field.
pub fn set_password(&mut self, password: bool, cx: &mut Context<Self>) {
if self.password != password {
self.password = password;
if !password {
self.revealed = false;
}
cx.notify();
}
}
/// Whether the field is a password field.
pub fn is_password(&self) -> bool {
self.password
}
/// Sets whether a password field currently shows its content.
pub fn set_revealed(&mut self, revealed: bool, cx: &mut Context<Self>) {
if self.revealed != revealed {
self.revealed = revealed;
cx.notify();
}
}
/// Whether a password field currently shows its content.
pub fn is_revealed(&self) -> bool {
self.revealed
}
/// Sets whether the field holds multiple lines.
pub fn set_multiline(&mut self, multiline: bool, cx: &mut Context<Self>) {
if self.multiline != multiline {
self.multiline = multiline;
self.scroll_offset = point(Pixels::ZERO, Pixels::ZERO);
cx.notify();
}
}
/// Whether the field holds multiple lines.
pub fn is_multiline(&self) -> bool {
self.multiline
}
/// Sets which keystroke submits a multiline field.
pub fn set_submit_on(&mut self, submit_on: SubmitOn, cx: &mut Context<Self>) {
if self.submit_on != submit_on {
self.submit_on = submit_on;
cx.notify();
}
}
/// Which keystroke submits a multiline field.
pub fn submit_on(&self) -> SubmitOn {
self.submit_on
}
/// Whether the painted line masks the buffer.
fn masked(&self) -> bool {
self.password && !self.revealed
}
/// Records `old_text` at `range` being replaced by `new_len` bytes.
fn push_history(&mut self, range: Range<usize>, old_text: String, new_len: usize) {
self.redo_stack.clear();
let now = Instant::now();
if let Some(last) = self.undo_stack.last_mut()
&& now.duration_since(last.timestamp) < self.group_interval
{
let last_end = last.range.start + last.new_len;
// Typing that continues right after the previous edit.
if range.start == last_end {
last.new_len += new_len;
last.timestamp = now;
return;
}
// A deletion of text that sat right before the previous edit.
if range.end == last.range.start && new_len == 0 {
last.range.start = range.start;
last.old_text = old_text + &last.old_text;
last.timestamp = now;
return;
}
// An edit fully inside the previous edit's replacement.
if range.start >= last.range.start && range.end <= last_end {
last.new_len += new_len;
last.new_len -= range.len();
last.timestamp = now;
return;
}
}
self.undo_stack.push(HistoryEntry {
range: range.start..range.start + new_len,
old_text,
new_len,
cursor: self.cursor,
anchor: self.anchor,
timestamp: now,
});
if self.undo_stack.len() > MAX_HISTORY {
self.undo_stack.remove(0);
}
}
/// The focus handle, for applications that focus the field programmatically.
pub fn focus_handle(&self) -> gpui::FocusHandle {
self.focus_handle.clone()
}
/// The selected byte range of the buffer.
fn selection(&self) -> Range<usize> {
self.cursor.min(self.anchor)..self.cursor.max(self.anchor)
}
fn notify(&mut self, window: &mut Window, cx: &mut Context<Self>) {
cx.notify();
self.scroll_to_caret_pending = true;
self.goal_x = None;
self.caret_blink.update(cx, |blink, cx| blink.pause(cx));
window.notify_text_input_changed(&self.focus_handle);
}
/// Scrolls the field by a wheel delta, clamped at paint.
fn scroll_by(
&mut self,
delta: gpui::Point<Pixels>,
window: &mut Window,
cx: &mut Context<Self>,
) -> bool {
if self.multiline {
self.scroll_offset.y = (self.scroll_offset.y + delta.y).max(Pixels::ZERO);
} else {
self.scroll_offset.x = (self.scroll_offset.x - delta.x).max(Pixels::ZERO);
}
let scrollable = self
.layout
.borrow()
.as_ref()
.is_some_and(|layout| layout.content_height > layout.visible_height);
cx.notify();
window.notify_text_input_changed(&self.focus_handle);
self.multiline && scrollable
}
/// Emits `Focused`/`Blurred` when the field's focus state changed since the last paint.
fn sync_focus(&mut self, focused: bool, cx: &mut Context<Self>) {
if self.focused == focused {
return;
}
self.focused = focused;
if focused {
self.caret_blink.update(cx, |blink, cx| blink.enable(cx));
} else {
self.caret_blink.update(cx, |blink, cx| blink.disable(cx));
}
cx.emit(if focused {
InputStateEvent::Focused
} else {
InputStateEvent::Blurred
});
}
/// Whether the caret should paint.
pub fn caret_visible(&mut self, focused: bool, cx: &mut Context<Self>) -> bool {
focused && !self.read_only && self.caret_blink.read(cx).visible()
}
/// Replaces a byte range with `text` and collapses the caret to its end.
fn replace_range(
&mut self,
range: Range<usize>,
text: &str,
window: &mut Window,
cx: &mut Context<Self>,
) {
if self.read_only {
return;
}
if range.is_empty() && text.is_empty() {
return;
}
let text = sanitize(text, self.multiline);
let old_text = self.text[range.clone()].to_string();
self.push_history(range.clone(), old_text, text.len());
self.text.replace_range(range.clone(), &text);
self.cursor = range.start + text.len();
self.anchor = self.cursor;
self.marked_range = None;
cx.emit(InputStateEvent::TextChanged);
self.notify(window, cx);
}
/// Deletes the selection, or one character/word before the caret.
fn delete_backward(&mut self, word: bool, window: &mut Window, cx: &mut Context<Self>) {
if !self.selection().is_empty() {
let range = self.selection();
self.replace_range(range, "", window, cx);
} else if self.cursor > 0 {
let start = if word {
previous_word_start(&self.text, self.cursor)
} else {
previous_grapheme_start(&self.text, self.cursor)
};
self.replace_range(start..self.cursor, "", window, cx);
}
}
/// Deletes the selection, or one character/word after the caret.
fn delete_forward(&mut self, word: bool, window: &mut Window, cx: &mut Context<Self>) {
if !self.selection().is_empty() {
let range = self.selection();
self.replace_range(range, "", window, cx);
} else if self.cursor < self.text.len() {
let end = if word {
next_word_end(&self.text, self.cursor)
} else {
next_grapheme_start(&self.text, self.cursor)
};
self.replace_range(self.cursor..end, "", window, cx);
}
}
/// Moves the caret, extending the selection when `extend`.
fn move_cursor(
&mut self,
to: usize,
extend: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.cursor = to.clamp(0, self.text.len());
if !extend {
self.anchor = self.cursor;
}
self.notify(window, cx);
}
/// Selects `range`.
fn select_range(&mut self, range: Range<usize>, window: &mut Window, cx: &mut Context<Self>) {
self.anchor = range.start;
self.cursor = range.end;
self.notify(window, cx);
}
/// Copies the selected text to the clipboard.
fn copy_selection(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> bool {
if self.password {
return false;
}
let range = self.selection();
if range.is_empty() {
return false;
}
cx.write_to_clipboard(ClipboardItem::new_string(
self.text[range.clone()].to_string(),
));
true
}
/// Deletes the selected range, if any.
fn delete_selection(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let range = self.selection();
if !range.is_empty() {
self.replace_range(range, "", window, cx);
}
}
/// Shapes the masked or plain buffer, wrapping to `wrap_width` when set,
/// with the paint-time text style.
fn shape_current_text(&self, wrap_width: Option<Pixels>, window: &Window) -> ShapedText {
let (text, mask_map) = if self.masked() {
masked_display(&self.text)
} else {
(self.text.clone(), None)
};
let text_style = window.text_style();
let font = Font {
family: text_style.font_family.clone(),
features: text_style.font_features.clone(),
weight: text_style.font_weight,
style: text_style.font_style,
fallbacks: text_style.font_fallbacks.clone(),
};
let font_size = text_style.font_size.to_pixels(window.rem_size());
let runs = [TextRun {
len: text.len(),
font,
color: text_style.color,
background_color: None,
underline: None,
strikethrough: None,
}];
let shaped =
window
.text_system()
.shape_text(text.into(), font_size, &runs, wrap_width, None);
let mut lines = Vec::new();
match shaped {
Ok(shaped) => {
let mut start = 0;
for line in shaped {
let end = start + line.len();
lines.push(ShapedLineEntry { start, line });
start = end + '\n'.len_utf8();
}
}
Err(err) => eprintln!("shaping input text failed: {err}"),
}
ShapedText { lines, mask_map }
}
/// Stores the paint geometry for hit-testing and the IME conversions.
fn store_layout(
&mut self,
text: ShapedText,
bounds: Bounds<Pixels>,
padding: FieldPadding,
line_height: Pixels,
) -> gpui::Point<Pixels> {
// The visual row table: one entry per wrapped row, top to bottom.
let mut rows = Vec::new();
let mut y = Pixels::ZERO;
for (line_ix, entry) in text.lines.iter().enumerate() {
let mut row_start = 0;
let boundaries =
entry.line.wrap_boundaries().iter().map(|boundary| {
entry.line.runs()[boundary.run_ix].glyphs[boundary.glyph_ix].index
});
for end in boundaries.chain([entry.line.len()]) {
rows.push(VisualRow {
range: entry.start + row_start..entry.start + end,
y,
line_ix,
});
y += line_height;
row_start = end;
}
}
let content_height = y;
let visible_height = if self.multiline {
(bounds.size.height - padding.top * 2.0).max(Pixels::ZERO)
} else {
bounds.size.height
};
let caret = if rows.is_empty() {
(Pixels::ZERO, Pixels::ZERO)
} else {
let display = display_index(&text.mask_map, self.selection().end);
let row_ix = row_ix_for_display(&rows, display);
let row = &rows[row_ix];
let entry = &text.lines[row.line_ix];
let shaped = &entry.line.unwrapped_layout;
let row_start = row.range.start - entry.start;
let x = shaped.x_for_index(display.min(row.range.end) - entry.start)
- shaped.x_for_index(row_start);
(row.y, x)
};
if self.multiline {
let max_scroll = (content_height - visible_height).max(Pixels::ZERO);
if self.scroll_to_caret_pending {
self.scroll_to_caret_pending = false;
let (caret_y, _) = caret;
if caret_y + line_height > self.scroll_offset.y + visible_height {
self.scroll_offset.y = caret_y + line_height - visible_height;
}
if caret_y < self.scroll_offset.y {
self.scroll_offset.y = caret_y;
}
}
self.scroll_offset.y = self.scroll_offset.y.clamp(Pixels::ZERO, max_scroll);
self.scroll_offset.x = Pixels::ZERO;
} else {
let visible = (bounds.size.width - padding.left - padding.right).max(Pixels::ZERO);
let line_width = text
.lines
.first()
.map(|entry| entry.line.width())
.unwrap_or(Pixels::ZERO);
let max_scroll = (line_width - visible).max(Pixels::ZERO);
if self.scroll_to_caret_pending {
self.scroll_to_caret_pending = false;
// Pull the scroll just far enough to reveal the caret on whichever edge it left.
let (_, caret_x) = caret;
if caret_x - self.scroll_offset.x > visible {
self.scroll_offset.x = caret_x - visible;
}
if caret_x < self.scroll_offset.x {
self.scroll_offset.x = caret_x;
}
}
self.scroll_offset.x = self.scroll_offset.x.clamp(Pixels::ZERO, max_scroll);
self.scroll_offset.y = Pixels::ZERO;
}
let origin = point(
bounds.left() + padding.left - self.scroll_offset.x,
if self.multiline {
bounds.top() + padding.top - self.scroll_offset.y
} else {
bounds.top() + (bounds.size.height - line_height) / 2.0
},
);
let text_width = (bounds.size.width - padding.left - padding.right).max(Pixels::ZERO);
*self.layout.borrow_mut() = Some(InputLayout {
lines: text.lines,
rows,
bounds,
origin,
mask_map: text.mask_map,
line_height,
text_width,
content_height,
visible_height,
});
origin
}
/// Paints the selection highlight behind the text.
fn paint_selection(&self, origin: gpui::Point<Pixels>, color: gpui::Hsla, window: &mut Window) {
let selection = self.selection();
if selection.is_empty() {
return;
}
let layout = self.layout.borrow();
let Some(layout) = layout.as_ref() else {
return;
};
let start = display_index(&layout.mask_map, selection.start);
let end = display_index(&layout.mask_map, selection.end);
for (row_ix, clipped, beyond) in layout.row_overlaps(start..end) {
let top = origin.y + layout.rows[row_ix].y;
let left = origin.x + layout.row_x(row_ix, clipped.start);
let right = origin.x
+ if beyond {
layout.text_width
} else {
layout.row_x(row_ix, clipped.end)
};
let rect =
Bounds::from_corners(point(left, top), point(right, top + layout.line_height));
window.paint_quad(fill(rect, color));
}
}
/// Paints the text itself.
fn paint_text(
&self,
origin: gpui::Point<Pixels>,
right_padding: Pixels,
window: &mut Window,
cx: &mut App,
) {
let layout = self.layout.borrow();
let Some(layout) = layout.as_ref() else {
return;
};
let mask = Bounds::from_corners(
point(layout.bounds.left(), layout.bounds.top()),
point(
layout.bounds.right() - right_padding,
layout.bounds.bottom(),
),
);
window.with_content_mask(Some(ContentMask { bounds: mask }), |window| {
let mut line_top = Pixels::ZERO;
for entry in &layout.lines {
let line_origin = point(origin.x, origin.y + line_top);
entry
.line
.paint(
line_origin,
layout.line_height,
TextAlign::Left,
None,
window,
cx,
)
.ok();
line_top += layout.line_height * (entry.line.wrap_boundaries().len() + 1) as f32;
}
});
}
/// Paints the caret at the cursor position.
fn paint_caret(
&self,
origin: gpui::Point<Pixels>,
font_size: Pixels,
color: gpui::Hsla,
window: &mut Window,
) {
let layout = self.layout.borrow();
let Some(layout) = layout.as_ref() else {
return;
};
let display = display_index(&layout.mask_map, self.cursor);
let Some((row_ix, caret_x)) = layout.row_for_index(display) else {
return;
};
let caret_top = origin.y + layout.rows[row_ix].y + (layout.line_height - font_size) / 2.0;
let caret = Bounds::from_corners(
point(origin.x + caret_x, caret_top),
point(origin.x + caret_x + px(1.5), caret_top + font_size),
);
window.paint_quad(fill(caret, color));
}
/// Paints the field contents; returns whether a drag-select is in progress.
fn paint(
&mut self,
text: ShapedText,
bounds: Bounds<Pixels>,
padding: FieldPadding,
colors: ColorTokens,
window: &mut Window,
cx: &mut Context<Self>,
) -> bool {
let focused = self.focus_handle.is_focused(window);
self.sync_focus(focused, cx);
let text_style = window.text_style();
let font_size = text_style.font_size.to_pixels(window.rem_size());
let line_height = text_style
.line_height
.to_pixels(text_style.font_size, window.rem_size());
let origin = self.store_layout(text, bounds, padding, line_height);
if focused && !self.selection().is_empty() {
self.paint_selection(origin, colors.selection, window);
}
self.paint_text(origin, padding.right, window, cx);
// The composition underline shows the in-progress IME text.
if let Some(range) = self.marked_range.clone()
&& !range.is_empty()
{
self.paint_marked_underline(origin, range, colors.foreground, window);
}
// A caret promises that typing lands there; in read-only it does not.
if focused && self.caret_visible(focused, cx) && self.selection().is_empty() {
self.paint_caret(origin, font_size, colors.foreground, window);
}
self.dragging
}
/// The text index at a window point, using the last painted layout.
fn index_at(&self, position: gpui::Point<Pixels>) -> usize {
let layout = self.layout.borrow();
let Some(layout) = layout.as_ref() else {
return 0;
};
real_index(&layout.mask_map, layout.index_at(position))
}
/// Extends the selection to `position` while drag-selecting.
fn drag_select_to(
&mut self,
position: gpui::Point<Pixels>,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.cursor = self.index_at(position);
self.notify(window, cx);
}
/// Paints the composition underline under the marked range.
fn paint_marked_underline(
&self,
origin: gpui::Point<Pixels>,
range: Range<usize>,
color: gpui::Hsla,
window: &mut Window,
) {
let layout = self.layout.borrow();
let Some(layout) = layout.as_ref() else {
return;
};
let start = display_index(&layout.mask_map, range.start);
let end = display_index(&layout.mask_map, range.end);
let thickness = px(1.0);
for (row_ix, clipped, beyond) in layout.row_overlaps(start..end) {
let top = origin.y + layout.rows[row_ix].y + layout.line_height - thickness;
let left = origin.x + layout.row_x(row_ix, clipped.start);
let right = origin.x
+ if beyond {
layout.text_width
} else {
layout.row_x(row_ix, clipped.end)
};
let rect = Bounds::from_corners(point(left, top), point(right, top + thickness));
window.paint_quad(fill(rect, color));
}
}
/// Register drag listeners.
fn register_drag_listeners(entity: &Entity<Self>, window: &mut Window) {
window.on_mouse_event({
let state = entity.clone();
move |event: &MouseMoveEvent, phase, window, cx| {
if phase != DispatchPhase::Bubble || event.pressed_button != Some(MouseButton::Left)
{
return;
}
state.update(cx, |state, cx| {
state.drag_select_to(event.position, window, cx)
});
}
});
window.on_mouse_event({
let state = entity.clone();
move |event: &MouseUpEvent, phase, _window, cx| {
if phase != DispatchPhase::Bubble || event.button != MouseButton::Left {
return;
}
state.update(cx, |state, _| state.dragging = false);
}
});
}
/// The mouse-down handler: starts a drag and places or extends the selection.
fn handle_mouse_down(
&mut self,
event: &MouseDownEvent,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.dragging = true;
let index = self.index_at(event.position);
if event.click_count >= 3 {
// Triple-click selects the clicked row, which is all of it when single-line.
let row_range = self.layout.borrow().as_ref().and_then(|layout| {
let display = display_index(&layout.mask_map, index);
layout.row_range_at(display).map(|range| {
real_index(&layout.mask_map, range.start)
..real_index(&layout.mask_map, range.end)
})
});
match row_range {
Some(range) => self.select_range(range, window, cx),
None => {
let len = self.text.len();
self.select_range(0..len, window, cx);
}
}
} else if event.click_count == 2 {
let start = previous_word_start(&self.text, index);
let end = next_word_end(&self.text, index);
self.select_range(start..end, window, cx);
} else if event.modifiers.shift {
self.cursor = index;
self.notify(window, cx);
} else {
self.cursor = index;
self.anchor = index;
self.notify(window, cx);
}
}
/// The key-down handler: inserts typed characters.
fn handle_key_down(
&mut self,
event: &KeyDownEvent,
window: &mut Window,
cx: &mut Context<Self>,
) {
if !self.focus_handle.contains_focused(window, cx) {
return;
}
let keystroke = &event.keystroke;
// Chars typed with a menu modifier are shortcuts, not text,
// and control characters (enter, tab) have their own semantics.
if let Some(text) = &keystroke.key_char
&& !keystroke.modifiers.platform
&& !keystroke.modifiers.control
&& text.chars().all(|ch| !ch.is_control())
{
let range = self.selection();
self.replace_range(range, text, window, cx);
cx.stop_propagation();
}
}
fn left(&mut self, _: &Left, window: &mut Window, cx: &mut Context<Self>) {
let to = previous_grapheme_start(&self.text, self.cursor);
self.move_cursor(to, false, window, cx);
}
fn select_left(&mut self, _: &SelectLeft, window: &mut Window, cx: &mut Context<Self>) {
let to = previous_grapheme_start(&self.text, self.cursor);
self.move_cursor(to, true, window, cx);
}
fn right(&mut self, _: &Right, window: &mut Window, cx: &mut Context<Self>) {
let to = next_grapheme_start(&self.text, self.cursor);
self.move_cursor(to, false, window, cx);
}
fn select_right(&mut self, _: &SelectRight, window: &mut Window, cx: &mut Context<Self>) {
let to = next_grapheme_start(&self.text, self.cursor);
self.move_cursor(to, true, window, cx);
}
fn word_left(&mut self, _: &WordLeft, window: &mut Window, cx: &mut Context<Self>) {
let to = previous_word_start(&self.text, self.cursor);
self.move_cursor(to, false, window, cx);
}
fn select_word_left(
&mut self,
_: &SelectWordLeft,
window: &mut Window,
cx: &mut Context<Self>,
) {
let to = previous_word_start(&self.text, self.cursor);
self.move_cursor(to, true, window, cx);
}
fn word_right(&mut self, _: &WordRight, window: &mut Window, cx: &mut Context<Self>) {
let to = next_word_end(&self.text, self.cursor);
self.move_cursor(to, false, window, cx);
}
fn select_word_right(
&mut self,
_: &SelectWordRight,
window: &mut Window,
cx: &mut Context<Self>,
) {
let to = next_word_end(&self.text, self.cursor);
self.move_cursor(to, true, window, cx);
}
fn up(&mut self, _: &Up, window: &mut Window, cx: &mut Context<Self>) {
self.move_vertical(-1, false, window, cx);
}
fn down(&mut self, _: &Down, window: &mut Window, cx: &mut Context<Self>) {
self.move_vertical(1, false, window, cx);
}
fn select_up(&mut self, _: &SelectUp, window: &mut Window, cx: &mut Context<Self>) {
self.move_vertical(-1, true, window, cx);
}
fn select_down(&mut self, _: &SelectDown, window: &mut Window, cx: &mut Context<Self>) {
self.move_vertical(1, true, window, cx);
}
/// Moves the caret one visual row up or down, keeping the goal column.
fn move_vertical(
&mut self,
delta: isize,
extend: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
if !self.multiline {
return;
}
let target = {
let layout = self.layout.borrow();
let Some(layout) = layout.as_ref() else {
return;
};
let display = display_index(&layout.mask_map, self.cursor);
let Some((row_ix, caret_x)) = layout.row_for_index(display) else {
return;
};
let target = row_ix as isize + delta;
let (display, goal_x) = if target < 0 {
(0, None)
} else if target as usize >= layout.rows.len() {
(display_index(&layout.mask_map, self.text.len()), None)
} else {
let goal_x = self.goal_x.unwrap_or(caret_x);
(layout.index_at_row_x(target as usize, goal_x), Some(goal_x))
};
Some((real_index(&layout.mask_map, display), goal_x))
};
let Some((to, goal_x)) = target else {
return;
};
self.move_cursor(to, extend, window, cx);
// Set after `move_cursor`, which resets the goal on every move.
self.goal_x = goal_x;
}
fn home(&mut self, _: &Home, window: &mut Window, cx: &mut Context<Self>) {
self.move_cursor(0, false, window, cx);
}
fn end(&mut self, _: &End, window: &mut Window, cx: &mut Context<Self>) {
self.move_cursor(self.text.len(), false, window, cx);
}
fn select_home(&mut self, _: &SelectHome, window: &mut Window, cx: &mut Context<Self>) {
self.move_cursor(0, true, window, cx);
}
fn select_end(&mut self, _: &SelectEnd, window: &mut Window, cx: &mut Context<Self>) {
self.move_cursor(self.text.len(), true, window, cx);
}
fn backspace(&mut self, _: &Backspace, window: &mut Window, cx: &mut Context<Self>) {
self.delete_backward(false, window, cx);
}
fn delete(&mut self, _: &Delete, window: &mut Window, cx: &mut Context<Self>) {
self.delete_forward(false, window, cx);
}
fn delete_word_left(
&mut self,
_: &DeleteWordLeft,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.delete_backward(true, window, cx);
}
fn delete_word_right(
&mut self,
_: &DeleteWordRight,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.delete_forward(true, window, cx);
}
fn delete_to_beginning_of_line(
&mut self,
_: &DeleteToBeginningOfLine,
window: &mut Window,
cx: &mut Context<Self>,
) {
let range = 0..self.cursor;
self.replace_range(range, "", window, cx);
}
fn select_all(&mut self, _: &SelectAll, window: &mut Window, cx: &mut Context<Self>) {
let len = self.text.len();
self.select_range(0..len, window, cx);
}
fn copy(&mut self, _: &Copy, window: &mut Window, cx: &mut Context<Self>) {
if self.selection().is_empty() {
// Let an outer handler copy its own selection instead.
cx.propagate();
return;
}
self.copy_selection(window, cx);
}
fn cut(&mut self, _: &Cut, window: &mut Window, cx: &mut Context<Self>) {
if self.read_only || self.password {
return;
}
if self.copy_selection(window, cx) {
self.delete_selection(window, cx);
}
}
fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
if let Some(item) = cx.read_from_clipboard()
&& let Some(text) = item.text()
{
let range = self.selection();
self.replace_range(range, &text, window, cx);
}
}
fn undo(&mut self, _: &Undo, window: &mut Window, cx: &mut Context<Self>) {
if self.read_only {
return;
}
let Some(entry) = self.undo_stack.pop() else {
return;
};
let inverse = entry.reverse(&mut self.text);
self.redo_stack.push(inverse);
self.cursor = entry.cursor.min(self.text.len());
self.anchor = entry.anchor.min(self.text.len());
cx.emit(InputStateEvent::TextChanged);
self.notify(window, cx);
}
fn redo(&mut self, _: &Redo, window: &mut Window, cx: &mut Context<Self>) {
if self.read_only {
return;
}
let Some(entry) = self.redo_stack.pop() else {
return;
};
let inverse = entry.reverse(&mut self.text);
self.undo_stack.push(inverse);
// The caret lands at the end of the restored text,
// not where it was before the original edit.
let caret = entry.range.start + entry.old_text.len();
self.cursor = caret.min(self.text.len());
self.anchor = self.cursor;
cx.emit(InputStateEvent::TextChanged);
self.notify(window, cx);
}
fn submit(&mut self, _: &Submit, _window: &mut Window, cx: &mut Context<Self>) {
// A multiline field in the default mode turns Enter into a newline
if self.multiline && self.submit_on == SubmitOn::ModifiedEnter {
let range = self.selection();
self.replace_range(range, "\n", _window, cx);
} else {
cx.emit(InputStateEvent::Submitted);
}
}
fn secondary_submit(
&mut self,
_: &SecondarySubmit,
_window: &mut Window,
cx: &mut Context<Self>,
) {
cx.emit(InputStateEvent::Submitted);
}
fn insert_newline(&mut self, _: &InsertNewline, window: &mut Window, cx: &mut Context<Self>) {
if self.multiline {
let range = self.selection();
self.replace_range(range, "\n", window, cx);
}
}
fn escape(&mut self, _: &Escape, window: &mut Window, cx: &mut Context<Self>) {
window.blur(cx);
}
}
impl EntityInputHandler for InputState {
fn text_for_range(
&mut self,
range_utf16: Range<usize>,
adjusted_range: &mut Option<Range<usize>>,
_window: &mut Window,
_cx: &mut Context<Self>,
) -> Option<String> {
let range = utf16_range_to_utf8(&self.text, range_utf16);
adjusted_range.replace(utf8_range_to_utf16(&self.text, range.clone()));
Some(self.text[range].to_string())
}
fn selected_text_range(
&mut self,
_ignore_disabled_input: bool,
_window: &mut Window,
_cx: &mut Context<Self>,
) -> Option<UTF16Selection> {
Some(UTF16Selection {
range: utf8_range_to_utf16(&self.text, self.selection()),
reversed: self.cursor < self.anchor,
})
}
fn marked_text_range(
&self,
_window: &mut Window,
_cx: &mut Context<Self>,
) -> Option<Range<usize>> {
self.marked_range
.clone()
.map(|range| utf8_range_to_utf16(&self.text, range))
}
fn unmark_text(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {
self.marked_range = None;
}
fn replace_text_in_range(
&mut self,
range_utf16: Option<Range<usize>>,
text: &str,
window: &mut Window,
cx: &mut Context<Self>,
) {
// Simulated typing delivers named keys' control characters here
if text.chars().all(|ch| ch.is_control()) {
return;
}
let range = range_utf16.map_or_else(
|| self.selection(),
|range| utf16_range_to_utf8(&self.text, range),
);
self.replace_range(range, text, window, cx);
}
fn replace_and_mark_text_in_range(
&mut self,
range_utf16: Option<Range<usize>>,
new_text: &str,
new_selected_range_utf16: Option<Range<usize>>,
window: &mut Window,
cx: &mut Context<Self>,
) {
if self.read_only {
return;
}
let range = range_utf16.map_or_else(
|| self.selection(),
|range| utf16_range_to_utf8(&self.text, range),
);
let new_text = sanitize(new_text, self.multiline);
let old_text = self.text[range.clone()].to_string();
self.push_history(range.clone(), old_text, new_text.len());
self.text.replace_range(range.clone(), &new_text);
let start = range.start;
let end = start + new_text.len();
self.marked_range = Some(start..end);
let selected_end = new_selected_range_utf16
.map(|selected| utf16_range_to_utf8(&self.text, selected).end)
.unwrap_or(end);
self.cursor = selected_end;
self.anchor = selected_end;
cx.emit(InputStateEvent::TextChanged);
self.notify(window, cx);
}
fn bounds_for_range(
&mut self,
range_utf16: Range<usize>,
_element_bounds: Bounds<Pixels>,
_window: &mut Window,
_cx: &mut Context<Self>,
) -> Option<Bounds<Pixels>> {
let layout = self.layout.borrow();
let layout = layout.as_ref()?;
let start = display_index(
&layout.mask_map,
utf16_to_utf8_offset(&self.text, range_utf16.start),
);
let end = display_index(
&layout.mask_map,
utf16_to_utf8_offset(&self.text, range_utf16.end),
);
// `origin` is already an absolute window point.
if start == end {
let (row_ix, x) = layout.row_for_index(start)?;
let top = layout.origin.y + layout.rows[row_ix].y;
return Some(Bounds::from_corners(
point(layout.origin.x + x, top),
point(layout.origin.x + x, top + layout.line_height),
));
}
let overlaps = layout.row_overlaps(start..end);
let (first_ix, first_range, _) = overlaps.first().cloned()?;
let (last_ix, last_range, last_beyond) = overlaps.last().cloned()?;
let left = layout.origin.x + layout.row_x(first_ix, first_range.start);
let right = layout.origin.x
+ if last_beyond {
layout.text_width
} else {
layout.row_x(last_ix, last_range.end)
};
Some(Bounds::from_corners(
point(left, layout.origin.y + layout.rows[first_ix].y),
point(
right,
layout.origin.y + layout.rows[last_ix].y + layout.line_height,
),
))
}
fn character_index_for_point(
&mut self,
window_point: gpui::Point<Pixels>,
_window: &mut Window,
_cx: &mut Context<Self>,
) -> Option<usize> {
let layout = self.layout.borrow();
let layout = layout.as_ref()?;
Some(real_index(&layout.mask_map, layout.index_at(window_point)).min(self.text.len()))
}
fn text_length_utf16(
&mut self,
_window: &mut Window,
_cx: &mut Context<Self>,
) -> Option<usize> {
Some(self.text.encode_utf16().count())
}
}
/// A text field bound to an application-owned [`InputState`].
#[derive(IntoElement)]
pub struct Input {
id: ElementId,
state: Entity<InputState>,
placeholder: SharedString,
size: Size,
disabled: bool,
a11y_label: Option<SharedString>,
/// Multiline field height in rows; ignored when the state is single-line.
rows: usize,
prefix: Option<AnyElement>,
suffix: Option<AnyElement>,
}
impl Input {
/// Creates a text field bound to `state`.
pub fn new<I>(id: I, state: Entity<InputState>) -> Self
where
I: Into<ElementId>,
{
Self {
id: id.into(),
state,
placeholder: SharedString::default(),
size: Size::default(),
disabled: false,
a11y_label: None,
rows: DEFAULT_ROWS,
prefix: None,
suffix: None,
}
}
/// Sets the placeholder shown when the field is empty and unfocused.
pub fn placeholder<P>(mut self, placeholder: P) -> Self
where
P: Into<SharedString>,
{
self.placeholder = placeholder.into();
self
}
/// Sets the height of a multiline field, in rows.
pub fn rows(mut self, rows: usize) -> Self {
self.rows = rows.max(1);
self
}
/// Renders the field inert, unfocusable, and dimmed; disabled always wins.
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
/// Overrides the accessibility label.
pub fn accessibility_label<L>(mut self, label: L) -> Self
where
L: Into<SharedString>,
{
self.a11y_label = Some(label.into());
self
}
/// Docks an element inside the field's leading edge.
pub fn prefix<E>(mut self, prefix: E) -> Self
where
E: IntoElement,
{
self.prefix = Some(prefix.into_any_element());
self
}
/// Docks an element inside the field's trailing edge.
pub fn suffix<E>(mut self, suffix: E) -> Self
where
E: IntoElement,
{
self.suffix = Some(suffix.into_any_element());
self
}
}
impl Sizable for Input {
fn apply_size(&mut self, size: Size) {
self.size = size;
}
}
impl RenderOnce for Input {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.theme();
let colors = theme.colors;
let radius = theme.radius;
let metrics = theme.sizes.resolve(self.size);
let focus_handle = self.state.read(cx).focus_handle.clone();
let focused = focus_handle.is_focused(window);
let is_empty = self.state.read(cx).text.is_empty();
let is_password = self.state.read(cx).is_password();
let revealed = self.state.read(cx).is_revealed();
let multiline = self.state.read(cx).is_multiline();
let small = theme.sizes.resolve(Size::Small);
let docked_height = small.control_height + BORDER_WIDTH * 2.0;
let edge_inset = ((metrics.input_height - docked_height) / 2.0).max(Pixels::ZERO);
let vertical_inset = metrics.dense_padding_x * 0.5;
let reveal_width = small.dense_padding_x * 2.0 + small.icon_size;
let field_padding = FieldPadding {
left: Pixels::ZERO,
right: if is_password {
(edge_inset + reveal_width - metrics.dense_padding_x).max(Pixels::ZERO)
} else {
Pixels::ZERO
},
top: vertical_inset,
};
let line_height = multiline.then(|| match window.text_style().line_height {
DefiniteLength::Fraction(ratio) => metrics.text_size * ratio,
DefiniteLength::Absolute(length) => length.to_pixels(window.rem_size()),
});
let field_height = line_height.map_or(metrics.input_height, |line_height| {
line_height * self.rows as f32 + vertical_inset * 2.0
}) + BORDER_WIDTH * 2.0;
let fallback_label = self
.a11y_label
.clone()
.or_else(|| (!self.placeholder.is_empty()).then_some(self.placeholder.clone()))
.unwrap_or_else(|| SharedString::from(self.id.to_string()));
let edge_zone = |name: &'static str, element: AnyElement| {
div()
.debug_selector(|| name.to_string())
.flex_none()
.h_full()
.flex()
.items_center()
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
.child(element)
};
h_flex()
.id(self.id.clone())
.debug_selector(|| "input-field".to_string())
.key_context(INPUT_CONTEXT)
.role(Role::TextInput)
.aria_label(fallback_label)
.relative()
.overflow_hidden()
.w_full()
.h(field_height)
.pl(if self.prefix.is_some() {
edge_inset
} else {
metrics.dense_padding_x
})
.pr(if self.suffix.is_some() {
edge_inset
} else {
metrics.dense_padding_x
})
.gap_1p5()
.rounded(radius.control)
.bg(colors.input)
.text_size(metrics.text_size)
.text_color(colors.foreground)
.border_1()
.border_color(if theme.borders.fields {
colors.border
} else {
colors.input
})
.when(self.disabled, |input| input.opacity(0.5))
.when(!self.disabled, |input| {
input.track_focus(&focus_handle).tab_stop(true)
})
.when(focused && !theme.borders.focus_ring, |input| {
input.border_color(colors.ring)
})
.when(focused && theme.borders.focus_ring, |input| {
input.shadow(focus_ring(colors.ring))
})
.when_some(self.prefix, |input, prefix| {
input.child(edge_zone("input-prefix", prefix))
})
.child(
div()
.relative()
.flex_1()
.min_w_0()
.h_full()
.overflow_hidden()
.flex()
.flex_row()
.items_center()
.when_some(line_height, |region, line_height| {
region.items_start().line_height(line_height)
})
.when(is_empty && !focused, |region| {
region.child(
div()
.text_color(colors.muted_foreground)
.when(multiline, |placeholder| placeholder.pt(vertical_inset))
.child(self.placeholder.clone()),
)
})
.child(
canvas(
{
let state = self.state.clone();
move |bounds, window, cx| {
// The hitbox backs the I-beam hover cursor.
let hitbox =
window.insert_hitbox(bounds, HitboxBehavior::Normal);
let wrap_width = multiline.then(|| {
(bounds.size.width
- field_padding.left
- field_padding.right)
.max(Pixels::ZERO)
});
let text =
state.read(cx).shape_current_text(wrap_width, window);
(text, hitbox)
}
},
{
let state = self.state.clone();
let focus = focus_handle.clone();
let disabled = self.disabled;
move |bounds, (text, hitbox), window, cx| {
if !disabled {
window.set_cursor_style(CursorStyle::IBeam, &hitbox);
}
let dragging = state.update(cx, |state, cx| {
state.paint(text, bounds, field_padding, colors, window, cx)
});
if disabled {
return;
}
window.handle_input(
&focus,
gpui::ElementInputHandler::new(bounds, state.clone()),
cx,
);
window.on_mouse_event({
let state = state.clone();
move |event: &ScrollEvent, phase, window, cx| {
if phase != DispatchPhase::Bubble
|| !bounds.contains(&event.position)
{
return;
}
let delta = match event.delta {
ScrollDelta::Pixels(delta) => delta,
ScrollDelta::Lines(lines) => point(
px(lines.x * SCROLL_LINES_HEIGHT),
px(lines.y * SCROLL_LINES_HEIGHT),
),
};
let consumed = state.update(cx, |state, cx| {
state.scroll_by(delta, window, cx)
});
if consumed {
cx.stop_propagation();
}
}
});
if dragging {
InputState::register_drag_listeners(&state, window);
}
}
},
)
.absolute()
.inset_0(),
),
)
.when_some(self.suffix, |input, suffix| {
input.child(edge_zone("input-suffix", suffix))
})
.when(is_password && !self.disabled, |input| {
input.child(
div()
.id((self.id.clone(), "input-reveal"))
.debug_selector(|| "input-reveal".to_string())
.absolute()
.right(edge_inset)
.top_0()
.bottom_0()
.flex()
.items_center()
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
.child(
Button::new((self.id.clone(), "input-reveal-button"))
.debug_selector(|| "input-reveal-button".to_string())
.icon(if revealed {
IconRole::EyeOff
} else {
IconRole::Eye
})
.variant(ButtonVariant::Ghost)
.small()
.accessibility_label(if revealed {
"Hide password"
} else {
"Show password"
})
.on_click({
let state = self.state.clone();
move |_, _, cx| {
state.update(cx, |state, cx| {
state.set_revealed(!state.is_revealed(), cx)
});
}
}),
),
)
})
.when(!self.disabled, |input| {
input
.on_mouse_down(MouseButton::Left, {
let mouse_down_handler = self
.state
.read(cx)
.mouse_down_handler
.clone()
.expect("input state always carries its mouse-down handler");
move |event: &MouseDownEvent, window, cx| {
mouse_down_handler(event, window, cx);
}
})
.on_key_down({
let key_down_handler = self
.state
.read(cx)
.key_down_handler
.clone()
.expect("input state always carries its key handler");
move |event: &KeyDownEvent, window, cx| {
key_down_handler(event, window, cx);
}
})
})
.on_action({
let state = self.state.clone();
move |action: &Escape, window, cx| {
state.update(cx, |state, cx| state.escape(action, window, cx))
}
})
.on_action({
let state = self.state.clone();
move |action: &Submit, window, cx| {
state.update(cx, |state, cx| state.submit(action, window, cx))
}
})
.on_action({
let state = self.state.clone();
move |action: &SecondarySubmit, window, cx| {
state.update(cx, |state, cx| state.secondary_submit(action, window, cx))
}
})
.on_action({
let state = self.state.clone();
move |action: &InsertNewline, window, cx| {
state.update(cx, |state, cx| state.insert_newline(action, window, cx))
}
})
.on_action({
let state = self.state.clone();
move |action: &Backspace, window, cx| {
state.update(cx, |state, cx| state.backspace(action, window, cx))
}
})
.on_action({
let state = self.state.clone();
move |action: &Delete, window, cx| {
state.update(cx, |state, cx| state.delete(action, window, cx))
}
})
.on_action({
let state = self.state.clone();
move |action: &DeleteWordLeft, window, cx| {
state.update(cx, |state, cx| state.delete_word_left(action, window, cx))
}
})
.on_action({
let state = self.state.clone();
move |action: &DeleteWordRight, window, cx| {
state.update(cx, |state, cx| state.delete_word_right(action, window, cx))
}
})
.on_action({
let state = self.state.clone();
move |action: &DeleteToBeginningOfLine, window, cx| {
state.update(cx, |state, cx| {
state.delete_to_beginning_of_line(action, window, cx)
})
}
})
.on_action({
let state = self.state.clone();
move |action: &Left, window, cx| {
state.update(cx, |state, cx| state.left(action, window, cx))
}
})
.on_action({
let state = self.state.clone();
move |action: &Right, window, cx| {
state.update(cx, |state, cx| state.right(action, window, cx))
}
})
.on_action({
let state = self.state.clone();
move |action: &SelectLeft, window, cx| {
state.update(cx, |state, cx| state.select_left(action, window, cx))
}
})
.on_action({
let state = self.state.clone();
move |action: &SelectRight, window, cx| {
state.update(cx, |state, cx| state.select_right(action, window, cx))
}
})
.on_action({
let state = self.state.clone();
move |action: &Up, window, cx| {
state.update(cx, |state, cx| state.up(action, window, cx))
}
})
.on_action({
let state = self.state.clone();
move |action: &Down, window, cx| {
state.update(cx, |state, cx| state.down(action, window, cx))
}
})
.on_action({
let state = self.state.clone();
move |action: &SelectUp, window, cx| {
state.update(cx, |state, cx| state.select_up(action, window, cx))
}
})
.on_action({
let state = self.state.clone();
move |action: &SelectDown, window, cx| {
state.update(cx, |state, cx| state.select_down(action, window, cx))
}
})
.on_action({
let state = self.state.clone();
move |action: &WordLeft, window, cx| {
state.update(cx, |state, cx| state.word_left(action, window, cx))
}
})
.on_action({
let state = self.state.clone();
move |action: &WordRight, window, cx| {
state.update(cx, |state, cx| state.word_right(action, window, cx))
}
})
.on_action({
let state = self.state.clone();
move |action: &SelectWordLeft, window, cx| {
state.update(cx, |state, cx| state.select_word_left(action, window, cx))
}
})
.on_action({
let state = self.state.clone();
move |action: &SelectWordRight, window, cx| {
state.update(cx, |state, cx| state.select_word_right(action, window, cx))
}
})
.on_action({
let state = self.state.clone();
move |action: &Home, window, cx| {
state.update(cx, |state, cx| state.home(action, window, cx))
}
})
.on_action({
let state = self.state.clone();
move |action: &End, window, cx| {
state.update(cx, |state, cx| state.end(action, window, cx))
}
})
.on_action({
let state = self.state.clone();
move |action: &SelectHome, window, cx| {
state.update(cx, |state, cx| state.select_home(action, window, cx))
}
})
.on_action({
let state = self.state.clone();
move |action: &SelectEnd, window, cx| {
state.update(cx, |state, cx| state.select_end(action, window, cx))
}
})
.on_action({
let state = self.state.clone();
move |action: &SelectAll, window, cx| {
state.update(cx, |state, cx| state.select_all(action, window, cx))
}
})
.on_action({
let state = self.state.clone();
move |action: &Copy, window, cx| {
state.update(cx, |state, cx| state.copy(action, window, cx))
}
})
.on_action({
let state = self.state.clone();
move |action: &Cut, window, cx| {
state.update(cx, |state, cx| state.cut(action, window, cx))
}
})
.on_action({
let state = self.state.clone();
move |action: &Paste, window, cx| {
state.update(cx, |state, cx| state.paste(action, window, cx))
}
})
.on_action({
let state = self.state.clone();
move |action: &Undo, window, cx| {
state.update(cx, |state, cx| state.undo(action, window, cx))
}
})
.on_action({
let state = self.state.clone();
move |action: &Redo, window, cx| {
state.update(cx, |state, cx| state.redo(action, window, cx))
}
})
}
}
/// Registers input keybindings on the application.
///
/// Pass `None` for the platform defaults.
pub fn bind_input_keys<B>(cx: &mut App, bindings: B)
where
B: Into<Option<InputBindings>>,
{
cx.bind_keys(bindings.into().unwrap_or_default().into_bindings());
}
/// The row holding `display`.
fn row_ix_for_display(rows: &[VisualRow], display: usize) -> usize {
rows.iter()
.position(|row| display <= row.range.end)
.unwrap_or(rows.len() - 1)
}
/// Builds the masked display: one bullet per grapheme, newlines kept as line breaks.
fn masked_display(text: &str) -> (String, Option<Vec<(usize, usize)>>) {
let mut display = String::new();
let mut map = Vec::with_capacity(text.len() + 1);
for (offset, grapheme) in text.grapheme_indices(true) {
if grapheme == "\n" {
display.push('\n');
map.push((offset, 1));
} else {
display.push_str(MASK);
map.push((offset, MASK_LEN));
}
}
// The end sentinel maps a click past the last glyph to the buffer end.
map.push((text.len(), 0));
(display, Some(map))
}
/// Maps a real buffer offset onto the masked display; identity when unmasked.
fn display_index(mask_map: &Option<Vec<(usize, usize)>>, real: usize) -> usize {
match mask_map {
None => real,
Some(map) => map
.iter()
.take_while(|&&(offset, _)| offset < real)
.map(|&(_, len)| len)
.sum(),
}
}
/// Maps a display offset from a click back onto the real buffer.
fn real_index(mask_map: &Option<Vec<(usize, usize)>>, display: usize) -> usize {
match mask_map {
None => display,
Some(map) => {
let mut consumed = 0;
for &(offset, len) in map {
if consumed >= display {
return offset;
}
consumed += len;
}
map.last().map(|&(offset, _)| offset).unwrap_or(0)
}
}
}
/// Newlines have no meaning in a single-line field; multiline fields keep
/// them and drop carriage returns.
fn sanitize(text: impl Into<String>, multiline: bool) -> String {
let text = text.into();
if multiline {
text.replace('\r', "")
} else {
text.replace('\n', " ").replace('\r', "")
}
}
/// Floors `index` to the nearest grapheme-cluster boundary at or below it.
fn previous_grapheme_start(text: &str, index: usize) -> usize {
text.grapheme_indices(true)
.rev()
.find(|(offset, _)| *offset < index)
.map(|(offset, _)| offset)
.unwrap_or(0)
}
/// Advances `index` to the next grapheme-cluster boundary.
fn next_grapheme_start(text: &str, index: usize) -> usize {
text.grapheme_indices(true)
.find(|(offset, _)| *offset > index)
.map(|(offset, _)| offset)
.unwrap_or(text.len())
}
/// The start of the word before `from`.
fn previous_word_start(text: &str, from: usize) -> usize {
let mut best = 0;
for (offset, word) in text.split_word_bound_indices() {
if offset >= from {
break;
}
if !word.starts_with(char::is_whitespace) {
best = offset;
}
}
best
}
/// The end of the word after `from`.
fn next_word_end(text: &str, from: usize) -> usize {
for (offset, word) in text.split_word_bound_indices() {
let end = offset + word.len();
if end > from && !word.starts_with(char::is_whitespace) {
return end;
}
}
text.len()
}
/// Converts a UTF-16 offset into a UTF-8 byte offset.
fn utf16_to_utf8_offset(text: &str, utf16_offset: usize) -> usize {
let mut utf16_count = 0;
for (byte_offset, char) in text.char_indices() {
if utf16_count >= utf16_offset {
return byte_offset;
}
utf16_count += char.len_utf16();
}
text.len()
}
/// Converts a UTF-8 byte range to a UTF-16 range.
fn utf8_range_to_utf16(text: &str, range: Range<usize>) -> Range<usize> {
utf16_to_utf8_offset(text, range.start)..utf16_to_utf8_offset(text, range.end)
}
/// Converts a UTF-16 range to a UTF-8 byte range.
fn utf16_range_to_utf8(text: &str, range: Range<usize>) -> Range<usize> {
utf16_to_utf8_offset(text, range.start)..utf16_to_utf8_offset(text, range.end)
}