use std::{borrow::Cow, ops::Range, time::Duration};
use gpui::{
App, Bounds, ClipboardItem, Context, CursorStyle, DispatchPhase, ElementId,
ElementInputHandler, Entity, EntityInputHandler, EventEmitter, FocusHandle, Focusable, Global,
GlobalElementId, KeyBinding, LayoutId, MouseButton, MouseDownEvent, MouseMoveEvent,
MouseUpEvent, PaintQuad, Pixels, Point, SharedString, Style, Task, TextRun, UTF16Selection,
UnderlineStyle, Window, WrappedLine, actions, div, fill, prelude::*, px, relative,
};
use unicode_segmentation::UnicodeSegmentation as _;
use theme::{HighlightKind, Metrics, SyntaxPalette, TextStyle, Theme};
pub mod caret;
mod chip;
mod edit;
mod element;
mod ime;
mod text;
pub use caret::{CaretHeight, CaretShape};
pub use chip::Chip;
pub use element::*;
pub use text::*;
actions!(
bezel_text_field,
[
Backspace,
Delete,
Left,
Right,
SelectLeft,
SelectRight,
SelectAll,
Home,
End,
SelectHome,
SelectEnd,
WordLeft,
WordRight,
SelectWordLeft,
SelectWordRight,
DeleteWordLeft,
DeleteWordRight,
DeleteToLineStart,
DeleteToLineEnd,
ShowCharacterPalette,
Paste,
Cut,
Copy,
Up,
Down,
SelectUp,
SelectDown,
InsertNewline,
Undo,
Redo,
]
);
pub const DEFAULT_UNDO_LIMIT: usize = 10;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FieldEvent {
Changed(Edit),
Moved,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Edit {
pub start: usize,
pub old_end: usize,
pub new_end: usize,
}
impl Edit {
pub fn between(before: &str, after: &str) -> Self {
let prefix = before
.char_indices()
.zip(after.chars())
.find(|((_, a), b)| a != b)
.map_or(before.len().min(after.len()), |((at, _), _)| at);
let suffix = before[prefix..]
.chars()
.rev()
.zip(after[prefix..].chars().rev())
.take_while(|(a, b)| a == b)
.map(|(a, _)| a.len_utf8())
.sum::<usize>();
Self {
start: prefix,
old_end: before.len() - suffix,
new_end: after.len() - suffix,
}
}
pub fn map(self, range: Range<usize>) -> Option<Range<usize>> {
let shift = |at: usize| at + self.new_end - self.old_end;
let mapped = if range.end <= self.start {
range
} else if range.start >= self.old_end {
shift(range.start)..shift(range.end)
} else if range.start < self.start && self.old_end < range.end {
range.start..shift(range.end)
} else if range.start < self.start {
range.start..self.start
} else if self.old_end < range.end {
self.new_end..shift(range.end)
} else {
return None;
};
(!mapped.is_empty()).then_some(mapped)
}
}
const BLINK: Duration = Duration::from_millis(500);
struct CaretBlink(bool);
impl Global for CaretBlink {}
pub(crate) fn caret_blink(cx: &App) -> bool {
cx.try_global::<CaretBlink>().is_none_or(|blink| blink.0)
}
pub(crate) fn set_caret_blink(blink: bool, cx: &mut App) {
cx.set_global(CaretBlink(blink));
cx.refresh_windows();
}
const CARET_WIDTH: Pixels = px(2.);
pub const KEY_CONTEXT: &str = "TextField";
pub const MULTILINE_KEY_CONTEXT: &str = "TextArea";
pub fn init(cx: &mut App) {
cx.bind_keys(bindings());
}
pub fn bindings() -> Vec<KeyBinding> {
let mut bindings = Vec::new();
let ctx = Some(KEY_CONTEXT);
bindings.extend([
KeyBinding::new("backspace", Backspace, ctx),
KeyBinding::new("delete", Delete, ctx),
KeyBinding::new("left", Left, ctx),
KeyBinding::new("right", Right, ctx),
KeyBinding::new("shift-left", SelectLeft, ctx),
KeyBinding::new("shift-right", SelectRight, ctx),
KeyBinding::new("home", Home, ctx),
KeyBinding::new("end", End, ctx),
KeyBinding::new("shift-home", SelectHome, ctx),
KeyBinding::new("shift-end", SelectEnd, ctx),
]);
let area = Some(MULTILINE_KEY_CONTEXT);
bindings.extend([
KeyBinding::new("enter", InsertNewline, area),
KeyBinding::new("up", Up, area),
KeyBinding::new("down", Down, area),
KeyBinding::new("shift-up", SelectUp, area),
KeyBinding::new("shift-down", SelectDown, area),
]);
#[cfg(target_os = "macos")]
bindings.extend([
KeyBinding::new("cmd-a", SelectAll, ctx),
KeyBinding::new("cmd-c", Copy, ctx),
KeyBinding::new("cmd-x", Cut, ctx),
KeyBinding::new("cmd-v", Paste, ctx),
KeyBinding::new("cmd-z", Undo, ctx),
KeyBinding::new("cmd-shift-z", Redo, ctx),
KeyBinding::new("ctrl-cmd-space", ShowCharacterPalette, ctx),
KeyBinding::new("cmd-left", Home, ctx),
KeyBinding::new("cmd-right", End, ctx),
KeyBinding::new("cmd-shift-left", SelectHome, ctx),
KeyBinding::new("cmd-shift-right", SelectEnd, ctx),
KeyBinding::new("alt-left", WordLeft, ctx),
KeyBinding::new("alt-right", WordRight, ctx),
KeyBinding::new("alt-shift-left", SelectWordLeft, ctx),
KeyBinding::new("alt-shift-right", SelectWordRight, ctx),
KeyBinding::new("cmd-backspace", DeleteToLineStart, ctx),
KeyBinding::new("alt-backspace", DeleteWordLeft, ctx),
KeyBinding::new("alt-delete", DeleteWordRight, ctx),
KeyBinding::new("ctrl-a", Home, ctx),
KeyBinding::new("ctrl-e", End, ctx),
KeyBinding::new("ctrl-b", Left, ctx),
KeyBinding::new("ctrl-f", Right, ctx),
KeyBinding::new("ctrl-h", Backspace, ctx),
KeyBinding::new("ctrl-d", Delete, ctx),
KeyBinding::new("ctrl-k", DeleteToLineEnd, ctx),
]);
#[cfg(target_os = "macos")]
bindings.extend([
KeyBinding::new("ctrl-n", Down, area),
KeyBinding::new("ctrl-p", Up, area),
]);
#[cfg(not(target_os = "macos"))]
bindings.extend([
KeyBinding::new("ctrl-a", SelectAll, ctx),
KeyBinding::new("ctrl-c", Copy, ctx),
KeyBinding::new("ctrl-x", Cut, ctx),
KeyBinding::new("ctrl-v", Paste, ctx),
KeyBinding::new("ctrl-left", WordLeft, ctx),
KeyBinding::new("ctrl-right", WordRight, ctx),
KeyBinding::new("ctrl-shift-left", SelectWordLeft, ctx),
KeyBinding::new("ctrl-shift-right", SelectWordRight, ctx),
KeyBinding::new("ctrl-backspace", DeleteWordLeft, ctx),
KeyBinding::new("ctrl-delete", DeleteWordRight, ctx),
KeyBinding::new("ctrl-z", Undo, ctx),
KeyBinding::new("ctrl-shift-z", Redo, ctx),
KeyBinding::new("ctrl-y", Redo, ctx),
]);
bindings
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Case {
#[default]
Mixed,
Upper,
}
impl Case {
fn apply(self, text: &str) -> Cow<'_, str> {
match self {
Self::Mixed => Cow::Borrowed(text),
Self::Upper => Cow::Owned(text.to_uppercase()),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Shape {
#[default]
Line,
Rows(usize),
Grow { min: usize, max: usize },
}
impl Shape {
fn is_multiline(self) -> bool {
!matches!(self, Self::Line)
}
}
#[derive(Clone)]
struct Snapshot {
content: SharedString,
selection: Range<usize>,
reversed: bool,
chips: Vec<Chip>,
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum EditKind {
Insert,
Delete,
}
pub struct TextField {
focus_handle: FocusHandle,
content: SharedString,
placeholder: SharedString,
shape: Shape,
case: Case,
selected_range: Range<usize>,
selection_reversed: bool,
marked_range: Option<Range<usize>>,
last_layout: Vec<WrappedLine>,
last_shown: Shown,
last_bounds: Option<Bounds<Pixels>>,
selecting: Option<(Granularity, Range<usize>)>,
goal_x: Option<Pixels>,
scroll: Point<Pixels>,
history: crate::history::SnapshotHistory<Snapshot>,
last_edit: Option<(EditKind, usize)>,
key_context: Option<SharedString>,
frame: bool,
metrics: Metrics,
caret_on: bool,
blink: Option<Task<()>>,
follow_caret: bool,
last_caret_shape: CaretShape,
spans: Vec<(Range<usize>, HighlightKind)>,
matches: Vec<Range<usize>>,
chips: Vec<Chip>,
}
impl EventEmitter<FieldEvent> for TextField {}
impl TextField {
pub fn new(cx: &mut Context<Self>) -> Self {
Self {
focus_handle: cx.focus_handle().tab_stop(true),
frame: true,
content: "".into(),
placeholder: "".into(),
shape: Shape::Line,
case: Case::default(),
selected_range: 0..0,
selection_reversed: false,
marked_range: None,
last_layout: Vec::new(),
last_shown: Shown::default(),
last_bounds: None,
selecting: None,
goal_x: None,
scroll: Point::default(),
history: crate::history::SnapshotHistory::new(DEFAULT_UNDO_LIMIT),
last_edit: None,
key_context: None,
metrics: TextStyle::Body.into(),
caret_on: true,
blink: None,
follow_caret: false,
last_caret_shape: CaretShape::Bar,
spans: Vec::new(),
matches: Vec::new(),
chips: Vec::new(),
}
}
pub fn with_undo_limit(mut self, limit: usize) -> Self {
self.history.set_limit(limit);
self
}
pub fn with_placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
self.placeholder = placeholder.into();
self
}
pub fn with_key_context(mut self, context: impl Into<SharedString>) -> Self {
self.key_context = Some(context.into());
self
}
pub fn with_frame(mut self, frame: bool) -> Self {
self.frame = frame;
self
}
pub fn with_metrics(mut self, metrics: Metrics) -> Self {
self.metrics = metrics;
self
}
pub fn set_metrics(&mut self, metrics: Metrics, cx: &mut Context<Self>) {
if self.metrics != metrics {
self.metrics = metrics;
cx.notify();
}
}
pub fn with_shape(mut self, shape: Shape) -> Self {
self.shape = shape;
self
}
pub fn with_case(mut self, case: Case) -> Self {
self.case = case;
self
}
pub fn set_case(&mut self, case: Case) {
self.case = case;
}
pub fn case(&self) -> Case {
self.case
}
pub fn shape(&self) -> Shape {
self.shape
}
pub fn content(&self) -> &SharedString {
&self.content
}
pub fn spans(&self) -> &[(Range<usize>, HighlightKind)] {
&self.spans
}
pub fn set_content(&mut self, content: impl Into<SharedString>, cx: &mut Context<Self>) {
let normalized = normalize(&content.into(), self.shape);
let before = self.content.len();
self.content = self.case.apply(&normalized).into_owned().into();
self.spans.clear();
self.matches.clear();
self.chips.clear();
self.history.clear();
self.last_edit = None;
let end = self.content.len();
self.selected_range = end..end;
self.marked_range = None;
cx.emit(FieldEvent::Changed(Edit {
start: 0,
old_end: before,
new_end: end,
}));
cx.notify();
}
pub fn clear(&mut self, cx: &mut Context<Self>) {
self.set_content("", cx);
}
fn edited(&mut self, edit: Edit, cx: &mut Context<Self>) {
let spans = std::mem::take(&mut self.spans);
self.spans = spans
.into_iter()
.filter_map(|(range, kind)| Some((edit.map(range)?, kind)))
.collect();
self.chips
.retain_mut(|chip| match chip::follow(edit, &chip.range) {
Some(range) => {
chip.range = range;
true
}
None => false,
});
cx.emit(FieldEvent::Changed(edit));
}
pub fn set_spans(&mut self, spans: Vec<(Range<usize>, HighlightKind)>, cx: &mut Context<Self>) {
self.spans = spans;
cx.notify();
}
pub fn set_matches(&mut self, matches: Vec<Range<usize>>, cx: &mut Context<Self>) {
self.matches = matches;
cx.notify();
}
pub fn set_chips(&mut self, mut chips: Vec<Chip>, cx: &mut Context<Self>) {
chips.sort_by_key(|chip| chip.range.start);
self.chips = chips;
self.selected_range = match self.selected_range.is_empty() {
true => {
let at = chip::outside(&self.chips, self.selected_range.start, false);
at..at
}
false => chip::widen(&self.chips, self.selected_range.clone()),
};
cx.notify();
}
pub fn chips(&self) -> &[Chip] {
&self.chips
}
pub fn select(&mut self, range: Range<usize>, cx: &mut Context<Self>) {
let end = floor_boundary(&self.content, range.end);
let start = floor_boundary(&self.content, range.start.min(end));
self.selected_range = match start == end {
true => {
let at = chip::outside(&self.chips, start, false);
at..at
}
false => chip::widen(&self.chips, start..end),
};
self.selection_reversed = false;
self.marked_range = None;
self.goal_x = None;
self.caret_moved();
cx.notify();
}
pub fn set_placeholder(
&mut self,
placeholder: impl Into<SharedString>,
cx: &mut Context<Self>,
) {
self.placeholder = placeholder.into();
cx.notify();
}
fn caret_moved(&mut self) {
self.follow_caret = true;
self.blink = None;
}
fn start_blink(&mut self, cx: &mut Context<Self>) {
self.caret_on = true;
self.blink = Some(cx.spawn(async move |field, cx| {
loop {
cx.background_executor().timer(BLINK).await;
let flipped = field.update(cx, |field, cx| {
field.caret_on = !field.caret_on;
cx.notify();
});
if flipped.is_err() {
break;
}
}
}));
}
pub fn cursor(&self) -> usize {
self.cursor_offset()
}
pub fn offset_bounds(&self, offset: usize) -> Option<Bounds<Pixels>> {
self.row_bounds(self.text_origin()?, offset..offset, self.line_height())
}
fn row_bounds(
&self,
origin: Point<Pixels>,
range: Range<usize>,
line_height: Pixels,
) -> Option<Bounds<Pixels>> {
let range = self.last_shown.range(&range);
let start = position_for_offset(&self.last_layout, range.start, line_height)?;
let end = position_for_offset(&self.last_layout, range.end, line_height)?;
Some(Bounds::from_corners(
origin + start,
origin + gpui::point(end.x, end.y + line_height),
))
}
fn move_to(&mut self, offset: usize, cx: &mut Context<Self>) {
let offset = chip::outside(&self.chips, offset, offset < self.cursor_offset());
self.selected_range = offset..offset;
self.goal_x = None;
self.caret_moved();
cx.emit(FieldEvent::Moved);
cx.notify()
}
fn cursor_offset(&self) -> usize {
if self.selection_reversed {
self.selected_range.start
} else {
self.selected_range.end
}
}
fn anchor_offset(&self) -> usize {
if self.selection_reversed {
self.selected_range.end
} else {
self.selected_range.start
}
}
fn select_span(&mut self, anchor: usize, head: usize, cx: &mut Context<Self>) {
self.selected_range = chip::widen(&self.chips, anchor.min(head)..anchor.max(head));
self.selection_reversed = head < anchor;
self.goal_x = None;
self.caret_moved();
cx.emit(FieldEvent::Moved);
cx.notify()
}
fn line_height(&self) -> Pixels {
px(self.metrics.line_height())
}
fn text_origin(&self) -> Option<Point<Pixels>> {
Some(self.last_bounds?.origin - self.scroll)
}
fn index_for_mouse_position(&self, position: Point<Pixels>, line_height: Pixels) -> usize {
if self.content.is_empty() || self.last_layout.is_empty() {
return 0;
}
let Some(origin) = self.text_origin() else {
return 0;
};
self.offset_at(position - origin, line_height)
}
fn chip_at(&self, position: Point<Pixels>, line_height: Pixels) -> Option<Range<usize>> {
let at = offset_for_position(
&self.last_layout,
position - self.text_origin()?,
line_height,
);
self.chips
.iter()
.map(|chip| chip.range.clone())
.find(|range| self.last_shown.range(range).contains(&at))
}
fn position_of(&self, offset: usize, line_height: Pixels) -> Option<Point<Pixels>> {
position_for_offset(&self.last_layout, self.last_shown.at(offset), line_height)
}
fn offset_at(&self, position: Point<Pixels>, line_height: Pixels) -> usize {
self.last_shown.offset(offset_for_position(
&self.last_layout,
position,
line_height,
))
}
fn select_to(&mut self, offset: usize, cx: &mut Context<Self>) {
let offset = chip::outside(&self.chips, offset, offset < self.cursor_offset());
self.goal_x = None;
self.caret_moved();
if self.selection_reversed {
self.selected_range.start = offset
} else {
self.selected_range.end = offset
};
if self.selected_range.end < self.selected_range.start {
self.selection_reversed = !self.selection_reversed;
self.selected_range = self.selected_range.end..self.selected_range.start;
}
cx.emit(FieldEvent::Moved);
cx.notify()
}
fn offset_to_utf16(&self, offset: usize) -> usize {
offset_to_utf16(&self.content, offset)
}
fn range_to_utf16(&self, range: &Range<usize>) -> Range<usize> {
range_to_utf16(&self.content, range.clone())
}
fn range_from_utf16(&self, range_utf16: &Range<usize>) -> Range<usize> {
range_from_utf16(&self.content, range_utf16.clone())
}
fn previous_boundary(&self, offset: usize) -> usize {
previous_boundary(&self.content, offset)
}
fn next_boundary(&self, offset: usize) -> usize {
next_boundary(&self.content, offset)
}
}
impl Focusable for TextField {
fn focus_handle(&self, _: &App) -> FocusHandle {
self.focus_handle.clone()
}
}