use std::{ops::Range, time::Duration};
use gpui::{
App, Bounds, ClipboardItem, Context, CursorStyle, 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::{Metrics, TextStyle, Theme};
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,
Moved,
}
const BLINK: Duration = Duration::from_millis(500);
struct CaretBlink(bool);
impl Global for CaretBlink {}
pub fn caret_blink(cx: &App) -> bool {
cx.try_global::<CaretBlink>().is_none_or(|blink| blink.0)
}
pub 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) {
let ctx = Some(KEY_CONTEXT);
cx.bind_keys([
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);
cx.bind_keys([
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")]
cx.bind_keys([
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")]
cx.bind_keys([
KeyBinding::new("ctrl-n", Down, area),
KeyBinding::new("ctrl-p", Up, area),
]);
#[cfg(not(target_os = "macos"))]
cx.bind_keys([
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),
]);
}
#[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,
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum EditKind {
Insert,
Delete,
}
pub struct TextField {
focus_handle: FocusHandle,
content: SharedString,
placeholder: SharedString,
shape: Shape,
selected_range: Range<usize>,
selection_reversed: bool,
marked_range: Option<Range<usize>>,
last_layout: Vec<WrappedLine>,
last_bounds: Option<Bounds<Pixels>>,
is_selecting: bool,
goal_x: Option<Pixels>,
scroll: Point<Pixels>,
undo: std::collections::VecDeque<Snapshot>,
redo: Vec<Snapshot>,
undo_limit: usize,
last_edit: Option<(EditKind, usize)>,
key_context: Option<SharedString>,
frame: bool,
metrics: Metrics,
caret_on: bool,
blink: Option<Task<()>>,
follow_caret: bool,
}
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,
selected_range: 0..0,
selection_reversed: false,
marked_range: None,
last_layout: Vec::new(),
last_bounds: None,
is_selecting: false,
goal_x: None,
scroll: Point::default(),
undo: std::collections::VecDeque::new(),
redo: Vec::new(),
undo_limit: DEFAULT_UNDO_LIMIT,
last_edit: None,
key_context: None,
metrics: TextStyle::Body.into(),
caret_on: true,
blink: None,
follow_caret: false,
}
}
pub fn with_undo_limit(mut self, limit: usize) -> Self {
self.undo_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 with_shape(mut self, shape: Shape) -> Self {
self.shape = shape;
self
}
pub fn shape(&self) -> Shape {
self.shape
}
pub fn content(&self) -> &SharedString {
&self.content
}
pub fn set_content(&mut self, content: impl Into<SharedString>, cx: &mut Context<Self>) {
self.content = normalize(&content.into(), self.shape).into();
self.undo.clear();
self.redo.clear();
self.last_edit = None;
let end = self.content.len();
self.selected_range = end..end;
self.marked_range = None;
cx.emit(FieldEvent::Changed);
cx.notify();
}
pub fn clear(&mut self, cx: &mut Context<Self>) {
self.set_content("", cx);
}
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, window: &Window) -> Option<Bounds<Pixels>> {
self.row_bounds(self.text_origin()?, offset..offset, window.line_height())
}
fn row_bounds(
&self,
origin: Point<Pixels>,
range: Range<usize>,
line_height: Pixels,
) -> Option<Bounds<Pixels>> {
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 left(&mut self, _: &Left, _: &mut Window, cx: &mut Context<Self>) {
if self.selected_range.is_empty() {
self.move_to(self.previous_boundary(self.cursor_offset()), cx);
} else {
self.move_to(self.selected_range.start, cx)
}
}
fn right(&mut self, _: &Right, _: &mut Window, cx: &mut Context<Self>) {
if self.selected_range.is_empty() {
self.move_to(self.next_boundary(self.selected_range.end), cx);
} else {
self.move_to(self.selected_range.end, cx)
}
}
fn select_left(&mut self, _: &SelectLeft, _: &mut Window, cx: &mut Context<Self>) {
self.select_to(self.previous_boundary(self.cursor_offset()), cx);
}
fn select_right(&mut self, _: &SelectRight, _: &mut Window, cx: &mut Context<Self>) {
self.select_to(self.next_boundary(self.cursor_offset()), cx);
}
fn select_all(&mut self, _: &SelectAll, _: &mut Window, cx: &mut Context<Self>) {
self.move_to(0, cx);
self.select_to(self.content.len(), cx)
}
fn home(&mut self, _: &Home, _: &mut Window, cx: &mut Context<Self>) {
self.move_to(line_start(&self.content, self.cursor_offset()), cx);
}
fn end(&mut self, _: &End, _: &mut Window, cx: &mut Context<Self>) {
self.move_to(line_end(&self.content, self.cursor_offset()), cx);
}
fn select_home(&mut self, _: &SelectHome, _: &mut Window, cx: &mut Context<Self>) {
self.select_to(line_start(&self.content, self.cursor_offset()), cx);
}
fn select_end(&mut self, _: &SelectEnd, _: &mut Window, cx: &mut Context<Self>) {
self.select_to(line_end(&self.content, self.cursor_offset()), cx);
}
fn up(&mut self, _: &Up, window: &mut Window, cx: &mut Context<Self>) {
self.vertical(-1, false, window, cx);
}
fn down(&mut self, _: &Down, window: &mut Window, cx: &mut Context<Self>) {
self.vertical(1, false, window, cx);
}
fn select_up(&mut self, _: &SelectUp, window: &mut Window, cx: &mut Context<Self>) {
self.vertical(-1, true, window, cx);
}
fn select_down(&mut self, _: &SelectDown, window: &mut Window, cx: &mut Context<Self>) {
self.vertical(1, true, window, cx);
}
fn vertical(&mut self, rows: i32, extend: bool, window: &mut Window, cx: &mut Context<Self>) {
if self.last_layout.is_empty() {
return;
}
let line_height = window.line_height();
let Some(at) = position_for_offset(&self.last_layout, self.cursor_offset(), line_height)
else {
return;
};
let goal = self.goal_x.unwrap_or(at.x);
let target = at.y + line_height * rows as f32;
let offset = if target < px(0.) {
0
} else {
offset_for_position(&self.last_layout, gpui::point(goal, target), line_height)
};
if extend {
self.select_to(offset, cx);
} else {
self.move_to(offset, cx);
}
self.goal_x = Some(goal);
}
fn insert_newline(&mut self, _: &InsertNewline, window: &mut Window, cx: &mut Context<Self>) {
if self.shape.is_multiline() {
self.replace_text_in_range(None, "\n", window, cx);
}
}
fn word_left(&mut self, _: &WordLeft, _: &mut Window, cx: &mut Context<Self>) {
self.move_to(
previous_word_boundary(&self.content, self.cursor_offset()),
cx,
);
}
fn word_right(&mut self, _: &WordRight, _: &mut Window, cx: &mut Context<Self>) {
self.move_to(next_word_boundary(&self.content, self.cursor_offset()), cx);
}
fn select_word_left(&mut self, _: &SelectWordLeft, _: &mut Window, cx: &mut Context<Self>) {
self.select_to(
previous_word_boundary(&self.content, self.cursor_offset()),
cx,
);
}
fn select_word_right(&mut self, _: &SelectWordRight, _: &mut Window, cx: &mut Context<Self>) {
self.select_to(next_word_boundary(&self.content, self.cursor_offset()), cx);
}
fn delete_word_left(
&mut self,
_: &DeleteWordLeft,
window: &mut Window,
cx: &mut Context<Self>,
) {
if self.selected_range.is_empty() {
self.select_to(
previous_word_boundary(&self.content, self.cursor_offset()),
cx,
);
}
self.replace_text_in_range(None, "", window, cx)
}
fn delete_word_right(
&mut self,
_: &DeleteWordRight,
window: &mut Window,
cx: &mut Context<Self>,
) {
if self.selected_range.is_empty() {
self.select_to(next_word_boundary(&self.content, self.cursor_offset()), cx);
}
self.replace_text_in_range(None, "", window, cx)
}
fn delete_to_line_start(
&mut self,
_: &DeleteToLineStart,
window: &mut Window,
cx: &mut Context<Self>,
) {
if self.selected_range.is_empty() {
self.select_to(line_start(&self.content, self.cursor_offset()), cx);
}
self.replace_text_in_range(None, "", window, cx)
}
fn delete_to_line_end(
&mut self,
_: &DeleteToLineEnd,
window: &mut Window,
cx: &mut Context<Self>,
) {
if self.selected_range.is_empty() {
self.select_to(line_end(&self.content, self.cursor_offset()), cx);
}
self.replace_text_in_range(None, "", window, cx)
}
fn backspace(&mut self, _: &Backspace, window: &mut Window, cx: &mut Context<Self>) {
if self.selected_range.is_empty() {
let prev = self.previous_boundary(self.cursor_offset());
if self.cursor_offset() == prev {
return;
}
self.select_to(prev, cx)
}
self.replace_text_in_range(None, "", window, cx)
}
fn delete(&mut self, _: &Delete, window: &mut Window, cx: &mut Context<Self>) {
if self.selected_range.is_empty() {
let next = self.next_boundary(self.cursor_offset());
if self.cursor_offset() == next {
return;
}
self.select_to(next, cx)
}
self.replace_text_in_range(None, "", window, cx)
}
fn on_mouse_down(
&mut self,
event: &MouseDownEvent,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.is_selecting = true;
let offset = self.index_for_mouse_position(event.position, window.line_height());
if event.modifiers.shift {
self.select_to(offset, cx);
} else {
self.move_to(offset, cx)
}
}
fn on_mouse_up(&mut self, _: &MouseUpEvent, _: &mut Window, _: &mut Context<Self>) {
self.is_selecting = false;
}
fn on_scroll_wheel(
&mut self,
event: &gpui::ScrollWheelEvent,
window: &mut Window,
cx: &mut Context<Self>,
) {
let delta = event.delta.pixel_delta(window.line_height());
self.scroll.x = (self.scroll.x - delta.x).max(px(0.));
self.scroll.y = (self.scroll.y - delta.y).max(px(0.));
cx.notify();
}
fn on_mouse_move(
&mut self,
event: &MouseMoveEvent,
window: &mut Window,
cx: &mut Context<Self>,
) {
if self.is_selecting {
let offset = self.index_for_mouse_position(event.position, window.line_height());
self.select_to(offset, cx);
}
}
fn show_character_palette(
&mut self,
_: &ShowCharacterPalette,
window: &mut Window,
_: &mut Context<Self>,
) {
window.show_character_palette();
}
fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
if let Some(text) = cx.read_from_clipboard().and_then(|item| item.text()) {
self.replace_text_in_range(None, &normalize(&text, self.shape), window, cx);
}
}
fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context<Self>) {
if !self.selected_range.is_empty() {
cx.write_to_clipboard(ClipboardItem::new_string(
self.content[self.selected_range.clone()].to_string(),
));
}
}
fn cut(&mut self, _: &Cut, window: &mut Window, cx: &mut Context<Self>) {
if !self.selected_range.is_empty() {
cx.write_to_clipboard(ClipboardItem::new_string(
self.content[self.selected_range.clone()].to_string(),
));
self.replace_text_in_range(None, "", window, cx)
}
}
fn snapshot(&self) -> Snapshot {
Snapshot {
content: self.content.clone(),
selection: self.selected_range.clone(),
reversed: self.selection_reversed,
}
}
fn restore(&mut self, point: Snapshot, cx: &mut Context<Self>) {
self.content = point.content;
self.selected_range = point.selection;
self.selection_reversed = point.reversed;
self.marked_range = None;
self.last_edit = None;
self.caret_moved();
cx.emit(FieldEvent::Changed);
cx.notify();
}
fn push_undo(&mut self, kind: EditKind, at: usize) {
if !joins_group(self.last_edit, kind, at) {
self.undo.push_back(self.snapshot());
while self.undo.len() > self.undo_limit {
self.undo.pop_front();
}
}
self.redo.clear();
}
fn undo(&mut self, _: &Undo, _: &mut Window, cx: &mut Context<Self>) {
let Some(point) = self.undo.pop_back() else {
return;
};
self.redo.push(self.snapshot());
self.restore(point, cx);
}
fn redo(&mut self, _: &Redo, _: &mut Window, cx: &mut Context<Self>) {
let Some(point) = self.redo.pop() else {
return;
};
self.undo.push_back(self.snapshot());
self.restore(point, cx);
}
fn move_to(&mut self, offset: usize, cx: &mut Context<Self>) {
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 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;
};
offset_for_position(&self.last_layout, position - origin, line_height)
}
fn select_to(&mut self, offset: usize, cx: &mut Context<Self>) {
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_from_utf16(&self, offset: usize) -> usize {
offset_from_utf16(&self.content, offset)
}
fn offset_to_utf16(&self, offset: usize) -> usize {
offset_to_utf16(&self.content, offset)
}
fn range_to_utf16(&self, range: &Range<usize>) -> Range<usize> {
self.offset_to_utf16(range.start)..self.offset_to_utf16(range.end)
}
fn range_from_utf16(&self, range_utf16: &Range<usize>) -> Range<usize> {
self.offset_from_utf16(range_utf16.start)..self.offset_from_utf16(range_utf16.end)
}
fn previous_boundary(&self, offset: usize) -> usize {
previous_boundary(&self.content, offset)
}
fn next_boundary(&self, offset: usize) -> usize {
next_boundary(&self.content, offset)
}
}
pub fn offset_from_utf16(text: &str, offset: usize) -> usize {
let mut utf8_offset = 0;
let mut utf16_count = 0;
for ch in text.chars() {
if utf16_count >= offset {
break;
}
utf16_count += ch.len_utf16();
utf8_offset += ch.len_utf8();
}
utf8_offset
}
pub fn offset_to_utf16(text: &str, offset: usize) -> usize {
let mut utf16_offset = 0;
let mut utf8_count = 0;
for ch in text.chars() {
if utf8_count >= offset {
break;
}
utf8_count += ch.len_utf8();
utf16_offset += ch.len_utf16();
}
utf16_offset
}
pub fn previous_boundary(text: &str, offset: usize) -> usize {
text.grapheme_indices(true)
.rev()
.find_map(|(idx, _)| (idx < offset).then_some(idx))
.unwrap_or(0)
}
pub fn next_boundary(text: &str, offset: usize) -> usize {
text.grapheme_indices(true)
.find_map(|(idx, _)| (idx > offset).then_some(idx))
.unwrap_or(text.len())
}
pub fn joins_group(last: Option<(EditKind, usize)>, kind: EditKind, at: usize) -> bool {
last.is_some_and(|(last_kind, offset)| last_kind == kind && at == offset)
}
pub fn normalize(text: &str, shape: Shape) -> String {
let text = text.replace("\r\n", "\n").replace('\r', "\n");
if shape.is_multiline() {
text
} else {
text.replace('\n', " ")
}
}
pub fn line_start(text: &str, offset: usize) -> usize {
text[..offset].rfind('\n').map_or(0, |at| at + 1)
}
pub fn line_end(text: &str, offset: usize) -> usize {
text[offset..]
.find('\n')
.map_or(text.len(), |at| offset + at)
}
fn is_word(segment: &str) -> bool {
segment.chars().any(char::is_alphanumeric)
}
pub fn previous_word_boundary(text: &str, offset: usize) -> usize {
text.split_word_bound_indices()
.filter(|(start, _)| *start < offset)
.rfind(|(_, segment)| is_word(segment))
.map(|(start, _)| start)
.unwrap_or(0)
}
pub fn next_word_boundary(text: &str, offset: usize) -> usize {
text.split_word_bound_indices()
.filter(|(start, segment)| start + segment.len() > offset)
.find(|(_, segment)| is_word(segment))
.map(|(start, segment)| start + segment.len())
.unwrap_or(text.len())
}
fn display_text(field: &TextField) -> (SharedString, bool) {
if field.content.is_empty() {
(field.placeholder.clone(), true)
} else {
(field.content.clone(), false)
}
}
fn lines_from(lines: &[WrappedLine]) -> impl Iterator<Item = (usize, &WrappedLine)> {
lines.iter().scan(0usize, |start, line| {
let at = *start;
*start = at + line.len() + 1;
Some((at, line))
})
}
fn rows(lines: &[WrappedLine], line_height: Pixels) -> Vec<(Range<usize>, Pixels)> {
let mut out = Vec::new();
let mut top = px(0.);
for (start, line) in lines_from(lines) {
let mut row_start = start;
for boundary in line.wrap_boundaries() {
let at = start + line.runs()[boundary.run_ix].glyphs[boundary.glyph_ix].index;
out.push((row_start..at, top));
row_start = at;
top += line_height;
}
out.push((row_start..start + line.len(), top));
top += line_height;
}
out
}
fn position_for_offset(
lines: &[WrappedLine],
offset: usize,
line_height: Pixels,
) -> Option<Point<Pixels>> {
let mut top = px(0.);
for (start, line) in lines_from(lines) {
if offset <= start + line.len() {
let local = line.position_for_index(offset.saturating_sub(start), line_height)?;
return Some(gpui::point(local.x, local.y + top));
}
top += line.size(line_height).height;
}
None
}
fn offset_for_position(
lines: &[WrappedLine],
position: Point<Pixels>,
line_height: Pixels,
) -> usize {
let mut top = px(0.);
let mut last = 0;
for (start, line) in lines_from(lines) {
let height = line.size(line_height).height;
last = start + line.len();
if position.y < top + height {
let local = gpui::point(position.x, position.y - top);
let (Ok(index) | Err(index)) = line.closest_index_for_position(local, line_height);
return start + index;
}
top += height;
}
last
}
fn selection_rows(
lines: &[WrappedLine],
range: &Range<usize>,
line_height: Pixels,
) -> Vec<Bounds<Pixels>> {
rows(lines, line_height)
.into_iter()
.filter(|(row, _)| range.start <= row.end && range.end >= row.start)
.filter_map(|(row, top)| {
let left = if range.start <= row.start {
px(0.)
} else {
position_for_offset(lines, range.start, line_height)?.x
};
let right = position_for_offset(lines, range.end.min(row.end), line_height)?.x;
(right > left).then(|| {
Bounds::from_corners(
gpui::point(left, top),
gpui::point(right, top + line_height),
)
})
})
.collect()
}
impl EntityInputHandler for TextField {
fn text_for_range(
&mut self,
range_utf16: Range<usize>,
actual_range: &mut Option<Range<usize>>,
_window: &mut Window,
_cx: &mut Context<Self>,
) -> Option<String> {
let range = self.range_from_utf16(&range_utf16);
actual_range.replace(self.range_to_utf16(&range));
Some(self.content[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: self.range_to_utf16(&self.selected_range),
reversed: self.selection_reversed,
})
}
fn marked_text_range(
&self,
_window: &mut Window,
_cx: &mut Context<Self>,
) -> Option<Range<usize>> {
self.marked_range
.as_ref()
.map(|range| self.range_to_utf16(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>>,
new_text: &str,
_: &mut Window,
cx: &mut Context<Self>,
) {
let range = range_utf16
.as_ref()
.map(|range_utf16| self.range_from_utf16(range_utf16))
.or(self.marked_range.clone())
.unwrap_or(self.selected_range.clone());
let kind = if new_text.is_empty() {
EditKind::Delete
} else {
EditKind::Insert
};
self.push_undo(
kind,
if new_text.is_empty() {
range.end
} else {
range.start
},
);
self.content =
(self.content[0..range.start].to_owned() + new_text + &self.content[range.end..])
.into();
self.selected_range = range.start + new_text.len()..range.start + new_text.len();
self.marked_range.take();
self.last_edit = Some((kind, self.selected_range.end));
self.caret_moved();
cx.emit(FieldEvent::Changed);
cx.notify();
}
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>,
) {
let range = range_utf16
.as_ref()
.map(|range_utf16| self.range_from_utf16(range_utf16))
.or(self.marked_range.clone())
.unwrap_or(self.selected_range.clone());
self.content =
(self.content[0..range.start].to_owned() + new_text + &self.content[range.end..])
.into();
self.marked_range =
(!new_text.is_empty()).then(|| range.start..range.start + new_text.len());
self.selected_range = new_selected_range_utf16
.as_ref()
.map(|range_utf16| self.range_from_utf16(range_utf16))
.map(|new_range| new_range.start + range.start..new_range.end + range.end)
.unwrap_or_else(|| range.start + new_text.len()..range.start + new_text.len());
self.caret_moved();
cx.emit(FieldEvent::Changed);
cx.notify();
}
fn bounds_for_range(
&mut self,
range_utf16: Range<usize>,
bounds: Bounds<Pixels>,
window: &mut Window,
_cx: &mut Context<Self>,
) -> Option<Bounds<Pixels>> {
let range = self.range_from_utf16(&range_utf16);
self.row_bounds(bounds.origin - self.scroll, range, window.line_height())
}
fn character_index_for_point(
&mut self,
point: Point<Pixels>,
window: &mut Window,
_cx: &mut Context<Self>,
) -> Option<usize> {
self.last_bounds?.localize(&point)?;
let origin = self.text_origin()?;
let offset = offset_for_position(&self.last_layout, point - origin, window.line_height());
Some(self.offset_to_utf16(offset))
}
}
impl Focusable for TextField {
fn focus_handle(&self, _: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl Render for TextField {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
if self.focus_handle.is_focused(_window) && caret_blink(cx) {
if self.blink.is_none() {
self.start_blink(cx);
}
} else {
self.blink = None;
self.caret_on = true;
}
let theme = Theme::of(cx);
let mut key_context = gpui::KeyContext::default();
key_context.add(KEY_CONTEXT);
if self.shape.is_multiline() {
key_context.add(MULTILINE_KEY_CONTEXT);
}
if let Some(extra) = self.key_context.clone() {
key_context.add(extra);
}
div()
.key_context(key_context)
.track_focus(&self.focus_handle(cx))
.cursor(CursorStyle::IBeam)
.on_action(cx.listener(Self::backspace))
.on_action(cx.listener(Self::delete))
.on_action(cx.listener(Self::left))
.on_action(cx.listener(Self::right))
.on_action(cx.listener(Self::select_left))
.on_action(cx.listener(Self::select_right))
.on_action(cx.listener(Self::select_all))
.on_action(cx.listener(Self::home))
.on_action(cx.listener(Self::end))
.on_action(cx.listener(Self::select_home))
.on_action(cx.listener(Self::select_end))
.on_action(cx.listener(Self::word_left))
.on_action(cx.listener(Self::word_right))
.on_action(cx.listener(Self::select_word_left))
.on_action(cx.listener(Self::select_word_right))
.on_action(cx.listener(Self::up))
.on_action(cx.listener(Self::down))
.on_action(cx.listener(Self::select_up))
.on_action(cx.listener(Self::select_down))
.on_action(cx.listener(Self::insert_newline))
.on_action(cx.listener(Self::undo))
.on_action(cx.listener(Self::redo))
.on_action(cx.listener(Self::delete_word_left))
.on_action(cx.listener(Self::delete_word_right))
.on_action(cx.listener(Self::delete_to_line_start))
.on_action(cx.listener(Self::delete_to_line_end))
.on_action(cx.listener(Self::show_character_palette))
.on_action(cx.listener(Self::paste))
.on_action(cx.listener(Self::cut))
.on_action(cx.listener(Self::copy))
.on_mouse_down(MouseButton::Left, cx.listener(Self::on_mouse_down))
.on_mouse_up(MouseButton::Left, cx.listener(Self::on_mouse_up))
.on_mouse_up_out(MouseButton::Left, cx.listener(Self::on_mouse_up))
.on_mouse_move(cx.listener(Self::on_mouse_move))
.on_scroll_wheel(cx.listener(Self::on_scroll_wheel))
.w_full()
.when(self.frame, |field| {
field
.px(px(10.0))
.py(px(7.0))
.rounded(px(Theme::button_radius()))
.bg(theme.input_bg)
.border_1()
.border_color(if self.focus_handle.is_focused(_window) {
theme.ring
} else {
theme.border
})
})
.text_size(px(self.metrics.size()))
.font_weight(self.metrics.weight)
.line_height(px(self.metrics.line_height()))
.text_color(theme.text)
.child(TextFieldElement { field: cx.entity() })
}
}
struct TextFieldElement {
field: Entity<TextField>,
}
struct FieldPrepaint {
lines: Vec<WrappedLine>,
origin: Point<Pixels>,
cursor: Option<PaintQuad>,
selection: Vec<PaintQuad>,
}
impl IntoElement for TextFieldElement {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
impl Element for TextFieldElement {
type RequestLayoutState = ();
type PrepaintState = FieldPrepaint;
fn id(&self) -> Option<ElementId> {
None
}
fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&gpui::InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, ()) {
let mut style = Style::default();
style.size.width = relative(1.).into();
let line_height = window.line_height();
let field = self.field.read(cx);
let shape = field.shape;
let (min, max) = match shape {
Shape::Line => {
style.size.height = line_height.into();
return (window.request_layout(style, [], cx), ());
}
Shape::Rows(rows) => {
style.size.height = (line_height * rows.max(1) as f32).into();
return (window.request_layout(style, [], cx), ());
}
Shape::Grow { min, max } => (min.max(1), max.max(min.max(1))),
};
let text = display_text(field).0;
let id = window.request_measured_layout(style, move |known, available, window, _cx| {
let text_style = window.text_style();
let font_size = text_style.font_size.to_pixels(window.rem_size());
let wrap_width = known.width.or(match available.width {
gpui::AvailableSpace::Definite(width) => Some(width),
_ => None,
});
let run = TextRun {
len: text.len(),
font: text_style.font(),
color: text_style.color,
background_color: None,
underline: None,
strikethrough: None,
};
let count = window
.text_system()
.shape_text(text.clone(), font_size, &[run], wrap_width, None)
.map(|lines| {
lines
.iter()
.map(|line| line.wrap_boundaries().len() + 1)
.sum::<usize>()
})
.unwrap_or(1);
gpui::size(
wrap_width.unwrap_or(px(0.)),
line_height * count.clamp(min, max) as f32,
)
});
(id, ())
}
fn prepaint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&gpui::InspectorElementId>,
bounds: Bounds<Pixels>,
_request_layout: &mut Self::RequestLayoutState,
window: &mut Window,
cx: &mut App,
) -> FieldPrepaint {
let theme = Theme::of(cx).clone();
let field = self.field.read(cx);
let selected_range = field.selected_range.clone();
let cursor = field.cursor_offset();
let shape = field.shape;
let marked_range = field.marked_range.clone();
let scrolled = field.scroll;
let follow_caret = field.follow_caret;
let style = window.text_style();
let (text, is_placeholder) = display_text(field);
let text_color = if is_placeholder {
theme.text_faint
} else {
style.color
};
let run = TextRun {
len: text.len(),
font: style.font(),
color: text_color,
background_color: None,
underline: None,
strikethrough: None,
};
let runs = if let Some(marked) = marked_range.as_ref() {
vec![
TextRun {
len: marked.start,
..run.clone()
},
TextRun {
len: marked.end - marked.start,
underline: Some(UnderlineStyle {
color: Some(run.color),
thickness: px(1.0),
wavy: false,
}),
..run.clone()
},
TextRun {
len: text.len() - marked.end,
..run
},
]
.into_iter()
.filter(|run| run.len > 0)
.collect()
} else {
vec![run]
};
let font_size = style.font_size.to_pixels(window.rem_size());
let line_height = window.line_height();
let wrap_width = shape.is_multiline().then_some(bounds.size.width);
let lines = window
.text_system()
.shape_text(text, font_size, &runs, wrap_width, None)
.map(|lines| lines.into_vec())
.unwrap_or_default();
let content_height: Pixels = lines.iter().map(|l| l.size(line_height).height).sum();
let content_width = lines.iter().map(|l| l.width()).fold(px(0.), Pixels::max);
let max = gpui::point(
(content_width - bounds.size.width).max(px(0.)),
(content_height - bounds.size.height).max(px(0.)),
);
let mut scroll = gpui::point(
scrolled.x.clamp(px(0.), max.x),
scrolled.y.clamp(px(0.), max.y),
);
if follow_caret && let Some(at) = position_for_offset(&lines, cursor, line_height) {
if at.y < scroll.y {
scroll.y = at.y;
} else if at.y + line_height > scroll.y + bounds.size.height {
scroll.y = at.y + line_height - bounds.size.height;
}
if at.x < scroll.x {
scroll.x = at.x;
} else if at.x + CARET_WIDTH > scroll.x + bounds.size.width {
scroll.x = at.x + CARET_WIDTH - bounds.size.width;
}
scroll.x = scroll.x.clamp(px(0.), max.x);
scroll.y = scroll.y.clamp(px(0.), max.y);
}
self.field.update(cx, |field, _| {
field.scroll = scroll;
field.follow_caret = false;
});
let origin = bounds.origin - scroll;
let (selection, cursor) = if selected_range.is_empty() {
let at = position_for_offset(&lines, cursor, line_height).unwrap_or_default();
(
Vec::new(),
Some(fill(
Bounds::new(
origin + at + gpui::point(px(0.), (line_height - font_size) / 2.),
gpui::size(CARET_WIDTH, font_size),
),
theme.caret,
)),
)
} else {
(
selection_rows(&lines, &selected_range, line_height)
.into_iter()
.map(|rect| {
fill(
Bounds::new(origin + rect.origin, rect.size),
theme.selection,
)
})
.collect(),
None,
)
};
FieldPrepaint {
lines,
origin,
cursor,
selection,
}
}
fn paint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&gpui::InspectorElementId>,
bounds: Bounds<Pixels>,
_request_layout: &mut Self::RequestLayoutState,
prepaint: &mut Self::PrepaintState,
window: &mut Window,
cx: &mut App,
) {
let focus_handle = self.field.read(cx).focus_handle.clone();
let caret_on = self.field.read(cx).caret_on;
window.handle_input(
&focus_handle,
ElementInputHandler::new(bounds, self.field.clone()),
cx,
);
let line_height = window.line_height();
let lines = std::mem::take(&mut prepaint.lines);
let selection = std::mem::take(&mut prepaint.selection);
let cursor = prepaint.cursor.take();
let origin = prepaint.origin;
window.with_content_mask(Some(gpui::ContentMask::new(bounds)), |window| {
for selection in selection {
window.paint_quad(selection);
}
let mut top = origin;
for line in &lines {
line.paint(top, line_height, gpui::TextAlign::Left, None, window, cx)
.ok();
top.y += line.size(line_height).height;
}
if focus_handle.is_focused(window)
&& caret_on
&& let Some(cursor) = cursor
{
window.paint_quad(cursor);
}
});
self.field.update(cx, |field, _| {
field.last_layout = lines;
field.last_bounds = Some(bounds);
});
}
}