diff --git a/crates/gpui_elements/src/editable_text/actions.rs b/crates/gpui_elements/src/editable_text/actions.rs index aac530dfba..a97bb7f658 100644 --- a/crates/gpui_elements/src/editable_text/actions.rs +++ b/crates/gpui_elements/src/editable_text/actions.rs @@ -1,4 +1,4 @@ -use gpui::{App, Window}; +use gpui::{App, AppContext, Window}; /// The key context used for input element keybindings. pub const DEFAULT_INPUT_CONTEXT: &str = "Input"; @@ -142,70 +142,94 @@ pub fn default_bindings() -> gpui::ActionBindingCollection { bindings } -pub trait EditableTextActionHandler { - fn escape(&mut self, _: &Escape, _w: &mut Window, _cx: &mut App) {} +pub trait EditableTextActionHandler<'app>: Sized { + type Context: AppContext; - fn insert_enter(&mut self, _: &Enter, _w: &mut Window, _cx: &mut App) {} - fn insert_tab(&mut self, _: &Tab, _w: &mut Window, _cx: &mut App) {} + fn escape(&mut self, _: &Escape, _w: &mut Window, _cx: &mut Self::Context) {} - fn backspace(&mut self, _: &Backspace, _w: &mut Window, _cx: &mut App) {} - fn delete(&mut self, _: &Delete, _w: &mut Window, _cx: &mut App) {} + fn insert_enter(&mut self, _: &Enter, _w: &mut Window, _cx: &mut Self::Context) {} + fn insert_tab(&mut self, _: &Tab, _w: &mut Window, _cx: &mut Self::Context) {} - fn delete_word_left(&mut self, _: &DeleteWordLeft, _w: &mut Window, _cx: &mut App) {} - fn delete_word_right(&mut self, _: &DeleteWordRight, _w: &mut Window, _cx: &mut App) {} + fn backspace(&mut self, _: &Backspace, _w: &mut Window, _cx: &mut Self::Context) {} + fn delete(&mut self, _: &Delete, _w: &mut Window, _cx: &mut Self::Context) {} + + fn delete_word_left(&mut self, _: &DeleteWordLeft, _w: &mut Window, _cx: &mut Self::Context) {} + fn delete_word_right(&mut self, _: &DeleteWordRight, _w: &mut Window, _cx: &mut Self::Context) { + } fn delete_to_line_start( &mut self, _: &DeleteToBeginningOfLine, _w: &mut Window, - _cx: &mut App, + _cx: &mut Self::Context, + ) { + } + fn delete_to_line_end( + &mut self, + _: &DeleteToEndOfLine, + _w: &mut Window, + _cx: &mut Self::Context, ) { } - fn delete_to_line_end(&mut self, _: &DeleteToEndOfLine, _w: &mut Window, _cx: &mut App) {} - fn nav_left(&mut self, _: &Left, _w: &mut Window, _cx: &mut App) {} - fn nav_right(&mut self, _: &Right, _w: &mut Window, _cx: &mut App) {} - fn nav_up(&mut self, _: &Up, _w: &mut Window, _cx: &mut App) {} - fn nav_down(&mut self, _: &Down, _w: &mut Window, _cx: &mut App) {} - fn nav_line_start(&mut self, _: &Home, _w: &mut Window, _cx: &mut App) {} - fn nav_line_end(&mut self, _: &End, _w: &mut Window, _cx: &mut App) {} - fn nav_start(&mut self, _: &MoveToBeginning, _w: &mut Window, _cx: &mut App) {} - fn nav_end(&mut self, _: &MoveToEnd, _w: &mut Window, _cx: &mut App) {} - fn nav_left_word(&mut self, _: &WordLeft, _w: &mut Window, _cx: &mut App) {} - fn nav_right_word(&mut self, _: &WordRight, _w: &mut Window, _cx: &mut App) {} + fn nav_left(&mut self, _: &Left, _w: &mut Window, _cx: &mut Self::Context) {} + fn nav_right(&mut self, _: &Right, _w: &mut Window, _cx: &mut Self::Context) {} + fn nav_up(&mut self, _: &Up, _w: &mut Window, _cx: &mut Self::Context) {} + fn nav_down(&mut self, _: &Down, _w: &mut Window, _cx: &mut Self::Context) {} + fn nav_line_start(&mut self, _: &Home, _w: &mut Window, _cx: &mut Self::Context) {} + fn nav_line_end(&mut self, _: &End, _w: &mut Window, _cx: &mut Self::Context) {} + fn nav_start(&mut self, _: &MoveToBeginning, _w: &mut Window, _cx: &mut Self::Context) {} + fn nav_end(&mut self, _: &MoveToEnd, _w: &mut Window, _cx: &mut Self::Context) {} + fn nav_left_word(&mut self, _: &WordLeft, _w: &mut Window, _cx: &mut Self::Context) {} + fn nav_right_word(&mut self, _: &WordRight, _w: &mut Window, _cx: &mut Self::Context) {} - fn select_all(&mut self, _: &SelectAll, _w: &mut Window, _cx: &mut App) {} - fn select_left(&mut self, _: &SelectLeft, _w: &mut Window, _cx: &mut App) {} - fn select_right(&mut self, _: &SelectRight, _w: &mut Window, _cx: &mut App) {} - fn select_up(&mut self, _: &SelectUp, _w: &mut Window, _cx: &mut App) {} - fn select_down(&mut self, _: &SelectDown, _w: &mut Window, _cx: &mut App) {} - fn select_start(&mut self, _: &SelectToBeginning, _w: &mut Window, _cx: &mut App) {} - fn select_end(&mut self, _: &SelectToEnd, _w: &mut Window, _cx: &mut App) {} - fn select_left_word(&mut self, _: &SelectWordLeft, _w: &mut Window, _cx: &mut App) {} - fn select_right_word(&mut self, _: &SelectWordRight, _w: &mut Window, _cx: &mut App) {} + fn select_all(&mut self, _: &SelectAll, _w: &mut Window, _cx: &mut Self::Context) {} + fn select_left(&mut self, _: &SelectLeft, _w: &mut Window, _cx: &mut Self::Context) {} + fn select_right(&mut self, _: &SelectRight, _w: &mut Window, _cx: &mut Self::Context) {} + fn select_up(&mut self, _: &SelectUp, _w: &mut Window, _cx: &mut Self::Context) {} + fn select_down(&mut self, _: &SelectDown, _w: &mut Window, _cx: &mut Self::Context) {} + fn select_start(&mut self, _: &SelectToBeginning, _w: &mut Window, _cx: &mut Self::Context) {} + fn select_end(&mut self, _: &SelectToEnd, _w: &mut Window, _cx: &mut Self::Context) {} + fn select_left_word(&mut self, _: &SelectWordLeft, _w: &mut Window, _cx: &mut Self::Context) {} + fn select_right_word(&mut self, _: &SelectWordRight, _w: &mut Window, _cx: &mut Self::Context) { + } - fn cut(&mut self, _: &Cut, _w: &mut Window, _cx: &mut App) {} - fn copy(&mut self, _: &Copy, _w: &mut Window, _cx: &mut App) {} - fn paste(&mut self, _: &Paste, _w: &mut Window, _cx: &mut App) {} + fn cut(&mut self, _: &Cut, _w: &mut Window, _cx: &mut Self::Context) {} + fn copy(&mut self, _: &Copy, _w: &mut Window, _cx: &mut Self::Context) {} + fn paste(&mut self, _: &Paste, _w: &mut Window, _cx: &mut Self::Context) {} - fn undo(&mut self, _: &Undo, _w: &mut Window, _cx: &mut App) {} - fn redo(&mut self, _: &Redo, _w: &mut Window, _cx: &mut App) {} + fn undo(&mut self, _: &Undo, _w: &mut Window, _cx: &mut Self::Context) {} + fn redo(&mut self, _: &Redo, _w: &mut Window, _cx: &mut Self::Context) {} fn show_character_palette( &mut self, _: &ShowCharacterPalette, window: &mut Window, - _cx: &mut App, + _cx: &mut Self::Context, ) { window.show_character_palette(); } fn on_mouse_down( &mut self, - _position: gpui::Point, + _event: &gpui::MouseDownEvent, + _text_position: gpui::Point, _w: &mut Window, - _cx: &mut App, + _cx: &mut Self::Context, + ) { + } + fn on_mouse_up( + &mut self, + _event: &gpui::MouseUpEvent, + _w: &mut Window, + _cx: &mut Self::Context, + ) { + } + fn on_mouse_move( + &mut self, + _event: &gpui::MouseMoveEvent, + _text_position: gpui::Point, + _w: &mut Window, + _cx: &mut Self::Context, ) { } - fn on_mouse_up(&mut self, _event: &gpui::MouseUpEvent, _w: &mut Window, _cx: &mut App) {} - fn on_mouse_move(&mut self, _event: &gpui::MouseMoveEvent, _w: &mut Window, _cx: &mut App) {} } diff --git a/crates/gpui_elements/src/editable_text/input_state.rs b/crates/gpui_elements/src/editable_text/input_state.rs index 854ee04569..2baceab4a0 100644 --- a/crates/gpui_elements/src/editable_text/input_state.rs +++ b/crates/gpui_elements/src/editable_text/input_state.rs @@ -1,6 +1,11 @@ -use crate::editable_text::{TextInputStateBase, notify::TextChanged}; +use super::notify::TextHistoryPushed; +use crate::editable_text::{ + EditableTextActionHandler, TextBoundary, TextInputStateBase, TextStateNotifier, + notify::TextChanged, +}; use gpui::{ - Bounds, Context, EntityInputHandler, EventEmitter, Pixels, Point, UTF16Selection, Window, + Bounds, Context, EntityInputHandler, EventEmitter, NavigationDirection, Pixels, Point, + UTF16Selection, Window, }; use std::ops::Range; @@ -9,6 +14,21 @@ pub struct TextInputState { } impl EventEmitter for TextInputState {} +impl EventEmitter for TextInputState {} + +impl TextStateNotifier for Context<'_, TextInputState> { + fn notify_changed(&mut self) { + self.notify(); + } + + fn emit_text_changed(&mut self, event: TextChanged) { + self.emit(event); + } + + fn emit_history(&mut self, event: TextHistoryPushed) { + self.emit(event); + } +} impl EntityInputHandler for TextInputState { fn text_for_range( @@ -52,11 +72,11 @@ impl EntityInputHandler for TextInputState { ) { let range_utf8 = self.internal.ime_resolve_range(range_utf16); self.internal - .replace_text_in_range_bytes(range_utf8, text_to_insert); + .replace_text_in_range_bytes(range_utf8, text_to_insert, cx); //self.mark_layout_dirty(); //cx.emit(CursorTrigger::PauseBlinkingForUserAction); - cx.emit(TextChanged); - cx.notify(); + cx.emit_text_changed(TextChanged); + cx.notify_changed(); } fn replace_and_mark_text_in_range( @@ -69,7 +89,7 @@ impl EntityInputHandler for TextInputState { ) { let range = self.internal.ime_resolve_range(range_utf16); self.internal - .replace_text_in_range_bytes(range.clone(), text_to_insert); + .replace_text_in_range_bytes(range.clone(), text_to_insert, cx); self.internal .ime_mark_text_in_range(&range, text_to_insert.len()); self.internal.ime_mark_selected_range( @@ -78,8 +98,8 @@ impl EntityInputHandler for TextInputState { text_to_insert.len(), ); //self.mark_layout_dirty(); - cx.emit(TextChanged); - cx.notify(); + cx.emit_text_changed(TextChanged); + cx.notify_changed(); } fn bounds_for_range( @@ -101,3 +121,243 @@ impl EntityInputHandler for TextInputState { unimplemented!() } } + +impl<'app> EditableTextActionHandler<'app> for TextInputState { + type Context = gpui::Context<'app, Self>; + + fn escape(&mut self, _: &super::Escape, window: &mut Window, cx: &mut Self::Context) { + self.internal.set_selected_range(0..0); + cx.notify(); + + window.blur(); + } + + fn insert_enter(&mut self, _: &super::Enter, _w: &mut Window, _cx: &mut Self::Context) {} + + fn insert_tab(&mut self, _: &super::Tab, window: &mut Window, cx: &mut Self::Context) { + self.replace_text_in_range(None, "\t", window, cx); + } + + fn backspace(&mut self, _: &super::Backspace, _: &mut Window, cx: &mut Self::Context) { + self.internal + .delete(NavigationDirection::Back, TextBoundary::Graphmeme, cx); + } + + fn delete(&mut self, _: &super::Delete, _w: &mut Window, cx: &mut Self::Context) { + self.internal + .delete(NavigationDirection::Forward, TextBoundary::Graphmeme, cx); + } + + fn delete_word_left( + &mut self, + _: &super::DeleteWordLeft, + _w: &mut Window, + cx: &mut Self::Context, + ) { + self.internal + .delete(NavigationDirection::Back, TextBoundary::Word, cx); + } + + fn delete_word_right( + &mut self, + _: &super::DeleteWordRight, + _w: &mut Window, + cx: &mut Self::Context, + ) { + self.internal + .delete(NavigationDirection::Forward, TextBoundary::Word, cx); + } + + fn delete_to_line_start( + &mut self, + _: &super::DeleteToBeginningOfLine, + _w: &mut Window, + cx: &mut Self::Context, + ) { + self.internal + .delete(NavigationDirection::Back, TextBoundary::Line, cx); + } + + fn delete_to_line_end( + &mut self, + _: &super::DeleteToEndOfLine, + _w: &mut Window, + cx: &mut Self::Context, + ) { + self.internal + .delete(NavigationDirection::Forward, TextBoundary::Line, cx); + } + + fn nav_left(&mut self, _: &super::Left, _w: &mut Window, cx: &mut Self::Context) { + self.internal + .nav_linear(NavigationDirection::Back, TextBoundary::Graphmeme, cx); + } + + fn nav_right(&mut self, _: &super::Right, _w: &mut Window, cx: &mut Self::Context) { + self.internal + .nav_linear(NavigationDirection::Forward, TextBoundary::Graphmeme, cx); + } + + fn nav_up(&mut self, _: &super::Up, _w: &mut Window, cx: &mut Self::Context) { + // semantically equivalent to line + self.internal + .nav_linear(NavigationDirection::Back, TextBoundary::Line, cx); + } + + fn nav_down(&mut self, _: &super::Down, _w: &mut Window, cx: &mut Self::Context) { + // semantically equivalent to line + self.internal + .nav_linear(NavigationDirection::Forward, TextBoundary::Line, cx); + } + + fn nav_line_start(&mut self, _: &super::Home, _w: &mut Window, cx: &mut Self::Context) { + // semantically equivalent to document + self.internal + .nav_linear(NavigationDirection::Back, TextBoundary::Line, cx); + } + + fn nav_line_end(&mut self, _: &super::End, _w: &mut Window, cx: &mut Self::Context) { + // semantically equivalent to document + self.internal + .nav_linear(NavigationDirection::Forward, TextBoundary::Line, cx); + } + + fn nav_start(&mut self, _: &super::MoveToBeginning, _w: &mut Window, cx: &mut Self::Context) { + self.internal + .nav_linear(NavigationDirection::Back, TextBoundary::Document, cx); + } + + fn nav_end(&mut self, _: &super::MoveToEnd, _w: &mut Window, cx: &mut Self::Context) { + self.internal + .nav_linear(NavigationDirection::Forward, TextBoundary::Document, cx); + } + + fn nav_left_word(&mut self, _: &super::WordLeft, _w: &mut Window, cx: &mut Self::Context) { + self.internal + .nav_linear(NavigationDirection::Back, TextBoundary::Word, cx); + } + + fn nav_right_word(&mut self, _: &super::WordRight, _w: &mut Window, cx: &mut Self::Context) { + self.internal + .nav_linear(NavigationDirection::Forward, TextBoundary::Word, cx); + } + + fn select_all(&mut self, _: &super::SelectAll, _w: &mut Window, cx: &mut Self::Context) { + self.internal.select_all(cx); + } + + fn select_left(&mut self, _: &super::SelectLeft, _w: &mut Window, cx: &mut Self::Context) { + self.internal + .select_linear(NavigationDirection::Back, TextBoundary::Graphmeme, cx); + } + + fn select_right(&mut self, _: &super::SelectRight, _w: &mut Window, cx: &mut Self::Context) { + self.internal + .select_linear(NavigationDirection::Forward, TextBoundary::Graphmeme, cx); + } + + fn select_up(&mut self, _: &super::SelectUp, _w: &mut Window, cx: &mut Self::Context) { + // semantically equivalent to select document + self.internal + .select_linear(NavigationDirection::Back, TextBoundary::Document, cx); + } + + fn select_down(&mut self, _: &super::SelectDown, _w: &mut Window, cx: &mut Self::Context) { + // semantically equivalent to select document + self.internal + .select_linear(NavigationDirection::Forward, TextBoundary::Document, cx); + } + + fn select_start( + &mut self, + _: &super::SelectToBeginning, + _w: &mut Window, + cx: &mut Self::Context, + ) { + self.internal + .select_linear(NavigationDirection::Back, TextBoundary::Document, cx); + } + + fn select_end(&mut self, _: &super::SelectToEnd, _w: &mut Window, cx: &mut Self::Context) { + self.internal + .select_linear(NavigationDirection::Forward, TextBoundary::Document, cx); + } + + fn select_left_word( + &mut self, + _: &super::SelectWordLeft, + _w: &mut Window, + cx: &mut Self::Context, + ) { + self.internal + .select_linear(NavigationDirection::Back, TextBoundary::Word, cx); + } + + fn select_right_word( + &mut self, + _: &super::SelectWordRight, + _w: &mut Window, + cx: &mut Self::Context, + ) { + self.internal + .select_linear(NavigationDirection::Forward, TextBoundary::Word, cx); + } + + fn cut(&mut self, _: &super::Cut, _w: &mut Window, cx: &mut Self::Context) { + self.internal.cut(cx); + } + + fn copy(&mut self, _: &super::Copy, _w: &mut Window, cx: &mut Self::Context) { + self.internal.copy(cx); + } + + fn paste(&mut self, _: &super::Paste, _w: &mut Window, cx: &mut Self::Context) { + self.internal.paste(cx); + } + + fn undo(&mut self, _: &super::Undo, _w: &mut Window, _cx: &mut Self::Context) { + // TODO: STUB + } + + fn redo(&mut self, _: &super::Redo, _w: &mut Window, _cx: &mut Self::Context) { + // TODO: STUB + } + + fn on_mouse_down( + &mut self, + event: &gpui::MouseDownEvent, + text_position: gpui::Point, + window: &mut Window, + cx: &mut Self::Context, + ) { + let character_pos = self.internal.caret_pos(); // TODO: Should be index_for_pixel_point + self.internal.on_mouse_down( + text_position, + character_pos, + event.click_count, + event.modifiers.shift, + window, + cx, + ); + } + + fn on_mouse_up( + &mut self, + _event: &gpui::MouseUpEvent, + _w: &mut Window, + _cx: &mut Self::Context, + ) { + self.internal.on_mouse_up(); + } + + fn on_mouse_move( + &mut self, + _event: &gpui::MouseMoveEvent, + text_position: Point, + _w: &mut Window, + cx: &mut Self::Context, + ) { + let character_pos = self.internal.caret_pos(); // TODO: Should be index_for_pixel_point + self.internal.on_mouse_move(character_pos, cx); + } +} diff --git a/crates/gpui_elements/src/editable_text/notify.rs b/crates/gpui_elements/src/editable_text/notify.rs index 4e5e01ec65..4fcb0467ff 100644 --- a/crates/gpui_elements/src/editable_text/notify.rs +++ b/crates/gpui_elements/src/editable_text/notify.rs @@ -15,7 +15,7 @@ impl TextHistoryPushed { pub fn new( range: Range, new_length: usize, - storage: impl UnicodeTextStorage, + storage: &dyn UnicodeTextStorage, selected_range: Range, ) -> Self { let timestamp = Instant::now(); diff --git a/crates/gpui_elements/src/editable_text/shared_state.rs b/crates/gpui_elements/src/editable_text/shared_state.rs index e4a975fb2a..b40428d140 100644 --- a/crates/gpui_elements/src/editable_text/shared_state.rs +++ b/crates/gpui_elements/src/editable_text/shared_state.rs @@ -1,7 +1,19 @@ -use crate::editable_text::UnicodeTextStorage; -use gpui::{App, FocusHandle, Focusable, NavigationDirection, Pixels, Point, UTF16Selection}; +use crate::editable_text::{ + TextBoundary, UnicodeTextStorage, + notify::{TextChanged, TextHistoryPushed}, +}; +use gpui::{ + App, AppContext, ClipboardItem, FocusHandle, Focusable, NavigationDirection, Pixels, Point, + UTF16Selection, Window, +}; use std::ops::Range; +pub trait TextStateNotifier { + fn notify_changed(&mut self); + fn emit_text_changed(&mut self, event: TextChanged); + fn emit_history(&mut self, event: TextHistoryPushed); +} + pub struct TextInputStateBase { storage: Box, @@ -72,6 +84,10 @@ impl TextInputStateBase { pub fn caret_pos(&self) -> usize { self.selected_range.start } + + pub fn set_selected_range(&mut self, range: Range) { + self.selected_range = range; + } } impl TextInputStateBase { @@ -117,17 +133,36 @@ impl TextInputStateBase { range.start.min(storage_len_utf8)..range.end.min(storage_len_utf8) } - pub fn replace_text(&mut self, start: usize, end: usize, new_text: &str) { + pub fn replace_text(&mut self, range: &Range, new_text: &str) { let storage_len_utf8 = self.storage.content_utf8().len(); - let start = start.min(storage_len_utf8); - let end = end.max(start).min(storage_len_utf8); + let start = range.start.min(storage_len_utf8); + let end = range.end.max(start).min(storage_len_utf8); self.storage.replace_range(start..end, new_text); let new_caret = start + new_text.len(); self.selected_range = new_caret..new_caret; } - pub fn replace_text_in_range_bytes(&mut self, range: Range, mut text_to_insert: &str) { + fn emit_change_for_undo( + &self, + cx: &mut impl TextStateNotifier, + range: Range, + length: usize, + ) { + cx.emit_history(TextHistoryPushed::new( + range.clone(), + length, + &*self.storage, + self.selected_range.clone(), + )); + } + + pub fn replace_text_in_range_bytes( + &mut self, + range: Range, + mut text_to_insert: &str, + cx: &mut impl TextStateNotifier, + ) { // TODO: Apply text sanitization // single-line fields should prune \n and \r // fields should be able to provide a max_length or other validations on text-input @@ -142,13 +177,9 @@ impl TextInputStateBase { text_to_insert = &text_to_insert[..text_to_insert.len().min(room)]; } - // TODO: Push history diff - // self.push_undo_patch(range.clone(), text_to_insert.len()); - + self.emit_change_for_undo(cx, range.clone(), text_to_insert.len()); self.storage.replace_range(range, text_to_insert); self.marked_range = None; - - // TODO: caller emits events } pub fn ime_mark_text_in_range(&mut self, range: &Range, text_len: usize) { @@ -178,3 +209,207 @@ impl TextInputStateBase { }; } } + +impl TextInputStateBase { + fn move_to(&mut self, caret_pos: usize) { + //cx.emit(CursorTrigger::PauseBlinkingForUserAction); + let caret_pos = caret_pos.min(self.storage.content_utf8().len()); + self.selected_range = caret_pos..caret_pos; + //self.scroll_to_cursor(); + //cx.notify_changed(); + } + + fn select_to(&mut self, caret_pos: usize) { + //cx.emit(CursorTrigger::PauseBlinkingForUserAction); + let caret_pos = caret_pos.min(self.storage().content_utf8().len()); + self.selected_range = caret_pos..self.selected_range.start; + //self.scroll_to_cursor(); + //cx.notify_changed(); + } + + pub fn delete( + &mut self, + direction: NavigationDirection, + boundary: TextBoundary, + cx: &mut impl TextStateNotifier, + ) { + let range = self.selected_range(); + let range = match range.is_empty() { + false => range, + true => self + .storage + .range_from_caret(self.caret_pos(), direction, boundary), + }; + + self.emit_change_for_undo(cx, range.clone(), 0); + + self.replace_text(&range, ""); + self.marked_range = None; + + cx.emit_text_changed(TextChanged); + cx.notify_changed(); + } + + pub fn nav_linear( + &mut self, + direction: NavigationDirection, + boundary: TextBoundary, + _cx: &mut impl TextStateNotifier, + ) { + let caret_pos = match self.selected_range.is_empty() { + false => match direction { + NavigationDirection::Back => self.selected_range.start, + NavigationDirection::Forward => self.selected_range.end, + }, + true => self + .storage + .offset_from_caret(self.caret_pos(), direction, boundary), + }; + self.move_to(caret_pos); + } + + pub fn select_all(&mut self, _cx: &mut impl TextStateNotifier) { + self.selected_range = 0..self.storage.content_utf8().len(); + } + + pub fn select_linear( + &mut self, + direction: NavigationDirection, + boundary: TextBoundary, + _cx: &mut impl TextStateNotifier, + ) { + let caret_pos = self + .storage + .offset_from_caret(self.caret_pos(), direction, boundary); + self.select_to(caret_pos); + } + + pub fn cut(&mut self, cx: &mut T) + where + T: TextStateNotifier + std::ops::Deref, + { + if !self.selected_range.is_empty() { + // Cut selected text + let slice = &self.storage.content_utf8()[self.selected_range.clone()]; + cx.write_to_clipboard(ClipboardItem::new_string(slice.to_string())); + self.replace_text_in_range_bytes(self.selected_range.clone(), "", cx); + } else { + // No selection: cut the entire current line (including newline) + let caret = self.caret_pos(); + let line_start = self.storage.find_line_start(caret); + let line_end = self.storage.find_line_end(caret); + let storage_len_utf8 = self.storage.content_utf8().len(); + + // Include the newline character if there is one after the line + let cut_end = if line_end < storage_len_utf8 { + line_end + 1 // Include the newline + } else if line_start > 0 { + // Last line with no trailing newline - include preceding newline instead + line_end + } else { + line_end + }; + + // For last line, also remove the preceding newline if it exists + let cut_start = if line_end >= storage_len_utf8 && line_start > 0 { + line_start - 1 // Include preceding newline for last line + } else { + line_start + }; + + self.selected_range = cut_start..cut_end; + + let slice = &self.storage.content_utf8()[self.selected_range.clone()]; + cx.write_to_clipboard(ClipboardItem::new_string(slice.to_string())); + + self.replace_text_in_range_bytes(self.selected_range.clone(), "", cx); + } + cx.emit_text_changed(TextChanged); + cx.notify_changed(); + } + + pub fn copy(&mut self, app: &mut App) { + if !self.selected_range.is_empty() { + let slice = &self.storage.content_utf8()[self.selected_range.clone()]; + app.write_to_clipboard(ClipboardItem::new_string(slice.to_string())); + } + } + + pub fn paste(&mut self, cx: &mut T) + where + T: TextStateNotifier + std::ops::Deref, + { + let Some(text) = cx.read_from_clipboard().and_then(|item| item.text()) else { + return; + }; + self.replace_text_in_range_bytes(self.ime_resolve_range(None), &text, cx); + cx.emit_text_changed(TextChanged); + cx.notify_changed(); + } + + pub fn on_mouse_down( + &mut self, + position: Point, + character_pos: usize, + click_count: usize, + shift: bool, + window: &mut Window, + cx: &mut Context, + ) where + Context: TextStateNotifier + std::ops::DerefMut, + { + window.focus(&self.focus_handle, cx); + self.is_selecting = true; + + let is_same_position = self + .last_click_position + .map(|last| { + let threshold = gpui::px(4.); + (position.x - last.x).abs() < threshold && (position.y - last.y).abs() < threshold + }) + .unwrap_or(false); + + if is_same_position && click_count > 1 { + self.click_count = click_count; + } else { + self.click_count = 1; + } + self.last_click_position = Some(position); + + match self.click_count { + 2 => { + let (word_start, word_end) = self.storage.word_range_at(character_pos); + self.selected_range = word_start..word_end; + //cx.notify(); + } + 3 => { + let line_start = self.storage.find_line_start(character_pos); + let line_end = self.storage.find_line_end(character_pos); + let line_end_with_newline = if line_end < self.storage.content_utf8().len() { + line_end + 1 + } else { + line_end + }; + self.selected_range = line_start..line_end_with_newline; + //cx.notify(); + } + _ => { + if shift { + self.select_to(character_pos); + } else { + self.move_to(character_pos); + } + } + } + } + + pub fn on_mouse_up(&mut self) { + self.is_selecting = false; + } + + pub fn on_mouse_move(&mut self, character_pos: usize, _cx: &mut impl TextStateNotifier) { + if self.is_selecting && self.click_count == 1 { + self.select_to(character_pos); + } + } +} diff --git a/crates/gpui_elements/src/editable_text/storage.rs b/crates/gpui_elements/src/editable_text/storage.rs index 1907d70107..1cd990cff8 100644 --- a/crates/gpui_elements/src/editable_text/storage.rs +++ b/crates/gpui_elements/src/editable_text/storage.rs @@ -1,6 +1,18 @@ +use gpui::NavigationDirection; use std::ops::Range; use unicode_segmentation::UnicodeSegmentation; +pub enum TextBoundary { + /// The next utf-8 character in a direction from the caret + Graphmeme, + /// The next word in a direction from the caret + Word, + /// The start/end of the current line + Line, + /// The rest of the text to the start/end of a document + Document, +} + pub trait UnicodeTextStorage { /// Returns a reference to the utf8 string. fn content_utf8(&self) -> &str; @@ -127,6 +139,57 @@ pub trait UnicodeTextStorage { len_utf8 } + /// Returns the utf-8 character position of first character after the first new-line preceeding the character at the provided utf-8 character position. + fn find_line_start(&self, position: usize) -> usize { + let content = self.content_utf8(); + content[..position.min(content.len())] + .rfind('\n') + .map(|pos| pos + 1) + .unwrap_or(0) + } + + /// Returns the utf-8 character position of the character immediately before the first new-line character after the character at the provided utf-8 character position. + fn find_line_end(&self, position: usize) -> usize { + let content = self.content_utf8(); + content[position.min(content.len())..] + .find('\n') + .map(|pos| position + pos) + .unwrap_or(content.len()) + } + + fn range_from_caret( + &self, + caret: usize, + direction: NavigationDirection, + magnitude: TextBoundary, + ) -> Range { + let offset = self.offset_from_caret(caret, direction, magnitude); + match direction { + NavigationDirection::Back => offset..caret, + NavigationDirection::Forward => caret..offset, + } + } + + fn offset_from_caret( + &self, + caret: usize, + direction: NavigationDirection, + magnitude: TextBoundary, + ) -> usize { + use NavigationDirection::*; + use TextBoundary::*; + match (direction, magnitude) { + (Back, Graphmeme) => self.previous_boundary(caret), + (Forward, Graphmeme) => self.next_boundary(caret), + (Back, Word) => self.previous_word_boundary(caret), + (Forward, Word) => self.next_word_boundary(caret), + (Back, Line) => self.find_line_start(caret), + (Forward, Line) => self.find_line_end(caret), + (Back, Document) => 0, + (Forward, Document) => self.content_utf8().len(), + } + } + fn word_range_at(&self, offset: usize) -> (usize, usize) { let offset = offset.min(self.content_utf8().len()); diff --git a/crates/gpui_elements/src/editable_text/text_area_state.rs b/crates/gpui_elements/src/editable_text/text_area_state.rs index e69de29bb2..628e135e73 100644 --- a/crates/gpui_elements/src/editable_text/text_area_state.rs +++ b/crates/gpui_elements/src/editable_text/text_area_state.rs @@ -0,0 +1,354 @@ +use crate::editable_text::{ + EditableTextActionHandler, TextBoundary, TextInputStateBase, TextStateNotifier, + notify::{TextChanged, TextHistoryPushed}, +}; +use gpui::{ + Bounds, Context, EntityInputHandler, EventEmitter, NavigationDirection, Pixels, Point, + UTF16Selection, Window, +}; +use std::ops::Range; + +pub struct TextAreaState { + internal: TextInputStateBase, +} + +impl EventEmitter for TextAreaState {} +impl EventEmitter for TextAreaState {} + +impl TextStateNotifier for Context<'_, TextAreaState> { + fn notify_changed(&mut self) { + self.notify(); + } + + fn emit_text_changed(&mut self, event: TextChanged) { + self.emit(event); + } + + fn emit_history(&mut self, event: TextHistoryPushed) { + self.emit(event); + } +} + +impl EntityInputHandler for TextAreaState { + fn text_for_range( + &mut self, + range_utf16: Range, + adjusted_range: &mut Option>, + _window: &mut Window, + _cx: &mut Context, + ) -> Option { + self.internal + .ime_text_for_range(range_utf16, adjusted_range) + } + + fn selected_text_range( + &mut self, + ignore_disabled_input: bool, + _window: &mut Window, + _cx: &mut Context, + ) -> Option { + self.internal.ime_selected_text_range(ignore_disabled_input) + } + + fn marked_text_range( + &self, + _window: &mut Window, + _cx: &mut Context, + ) -> Option> { + self.internal.ime_marked_text_range() + } + + fn unmark_text(&mut self, _window: &mut Window, _cx: &mut Context) { + self.internal.ime_unmark_text(); + } + + fn replace_text_in_range( + &mut self, + range_utf16: Option>, + text_to_insert: &str, + _window: &mut Window, + cx: &mut Context, + ) { + let range_utf8 = self.internal.ime_resolve_range(range_utf16); + self.internal + .replace_text_in_range_bytes(range_utf8, text_to_insert, cx); + //self.mark_layout_dirty(); + //cx.emit(CursorTrigger::PauseBlinkingForUserAction); + cx.emit_text_changed(TextChanged); + cx.notify_changed(); + } + + fn replace_and_mark_text_in_range( + &mut self, + range_utf16: Option>, + text_to_insert: &str, + new_selected_range_utf16: Option>, + _window: &mut Window, + cx: &mut Context, + ) { + let range = self.internal.ime_resolve_range(range_utf16); + self.internal + .replace_text_in_range_bytes(range.clone(), text_to_insert, cx); + self.internal + .ime_mark_text_in_range(&range, text_to_insert.len()); + self.internal.ime_mark_selected_range( + &range, + &new_selected_range_utf16, + text_to_insert.len(), + ); + //self.mark_layout_dirty(); + cx.emit_text_changed(TextChanged); + cx.notify_changed(); + } + + fn bounds_for_range( + &mut self, + range_utf16: Range, + bounds: Bounds, + _window: &mut Window, + _cx: &mut Context, + ) -> Option> { + unimplemented!() + } + + fn character_index_for_point( + &mut self, + point: Point, + _window: &mut Window, + _cx: &mut Context, + ) -> Option { + unimplemented!() + } +} + +impl<'app> EditableTextActionHandler<'app> for TextAreaState { + type Context = gpui::Context<'app, Self>; + + fn escape(&mut self, _: &super::Escape, window: &mut Window, cx: &mut Self::Context) { + self.internal.set_selected_range(0..0); + cx.notify(); + + window.blur(); + } + + fn insert_enter(&mut self, _: &super::Enter, window: &mut Window, cx: &mut Self::Context) { + self.replace_text_in_range(None, "\n", window, cx); + } + + fn insert_tab(&mut self, _: &super::Tab, window: &mut Window, cx: &mut Self::Context) { + self.replace_text_in_range(None, "\t", window, cx); + } + + fn backspace(&mut self, _: &super::Backspace, _w: &mut Window, cx: &mut Self::Context) { + self.internal + .delete(NavigationDirection::Back, TextBoundary::Graphmeme, cx); + } + + fn delete(&mut self, _: &super::Delete, _w: &mut Window, cx: &mut Self::Context) { + self.internal + .delete(NavigationDirection::Forward, TextBoundary::Graphmeme, cx); + } + + fn delete_word_left( + &mut self, + _: &super::DeleteWordLeft, + _w: &mut Window, + cx: &mut Self::Context, + ) { + self.internal + .delete(NavigationDirection::Back, TextBoundary::Word, cx); + } + + fn delete_word_right( + &mut self, + _: &super::DeleteWordRight, + _w: &mut Window, + cx: &mut Self::Context, + ) { + self.internal + .delete(NavigationDirection::Forward, TextBoundary::Word, cx); + } + + fn delete_to_line_start( + &mut self, + _: &super::DeleteToBeginningOfLine, + _w: &mut Window, + cx: &mut Self::Context, + ) { + self.internal + .delete(NavigationDirection::Back, TextBoundary::Line, cx); + } + + fn delete_to_line_end( + &mut self, + _: &super::DeleteToEndOfLine, + _w: &mut Window, + cx: &mut Self::Context, + ) { + self.internal + .delete(NavigationDirection::Forward, TextBoundary::Line, cx); + } + + fn nav_left(&mut self, _: &super::Left, _w: &mut Window, cx: &mut Self::Context) { + self.internal + .nav_linear(NavigationDirection::Back, TextBoundary::Graphmeme, cx); + } + + fn nav_right(&mut self, _: &super::Right, _w: &mut Window, cx: &mut Self::Context) { + self.internal + .nav_linear(NavigationDirection::Forward, TextBoundary::Graphmeme, cx); + } + + fn nav_up(&mut self, _: &super::Up, _w: &mut Window, cx: &mut Self::Context) { + // TODO: implement + } + + fn nav_down(&mut self, _: &super::Down, _w: &mut Window, cx: &mut Self::Context) { + // TODO: implement + } + + fn nav_line_start(&mut self, _: &super::Home, _w: &mut Window, cx: &mut Self::Context) { + self.internal + .nav_linear(NavigationDirection::Back, TextBoundary::Line, cx); + } + + fn nav_line_end(&mut self, _: &super::End, _w: &mut Window, cx: &mut Self::Context) { + self.internal + .nav_linear(NavigationDirection::Forward, TextBoundary::Line, cx); + } + + fn nav_start(&mut self, _: &super::MoveToBeginning, _w: &mut Window, cx: &mut Self::Context) { + self.internal + .nav_linear(NavigationDirection::Back, TextBoundary::Document, cx); + } + + fn nav_end(&mut self, _: &super::MoveToEnd, _w: &mut Window, cx: &mut Self::Context) { + self.internal + .nav_linear(NavigationDirection::Forward, TextBoundary::Document, cx); + } + + fn nav_left_word(&mut self, _: &super::WordLeft, _w: &mut Window, cx: &mut Self::Context) { + self.internal + .nav_linear(NavigationDirection::Back, TextBoundary::Word, cx); + } + + fn nav_right_word(&mut self, _: &super::WordRight, _w: &mut Window, cx: &mut Self::Context) { + self.internal + .nav_linear(NavigationDirection::Forward, TextBoundary::Word, cx); + } + + fn select_all(&mut self, _: &super::SelectAll, _w: &mut Window, cx: &mut Self::Context) { + self.internal.select_all(cx); + } + + fn select_left(&mut self, _: &super::SelectLeft, _w: &mut Window, cx: &mut Self::Context) { + self.internal + .select_linear(NavigationDirection::Back, TextBoundary::Graphmeme, cx); + } + + fn select_right(&mut self, _: &super::SelectRight, _w: &mut Window, cx: &mut Self::Context) { + self.internal + .select_linear(NavigationDirection::Forward, TextBoundary::Graphmeme, cx); + } + + fn select_up(&mut self, _: &super::SelectUp, _w: &mut Window, cx: &mut Self::Context) { + // TODO: implement + } + + fn select_down(&mut self, _: &super::SelectDown, _w: &mut Window, cx: &mut Self::Context) { + // TODO: implement + } + + fn select_start( + &mut self, + _: &super::SelectToBeginning, + _w: &mut Window, + cx: &mut Self::Context, + ) { + self.internal + .select_linear(NavigationDirection::Back, TextBoundary::Document, cx); + } + + fn select_end(&mut self, _: &super::SelectToEnd, _w: &mut Window, cx: &mut Self::Context) { + self.internal + .select_linear(NavigationDirection::Forward, TextBoundary::Document, cx); + } + + fn select_left_word( + &mut self, + _: &super::SelectWordLeft, + _w: &mut Window, + cx: &mut Self::Context, + ) { + self.internal + .select_linear(NavigationDirection::Back, TextBoundary::Word, cx); + } + + fn select_right_word( + &mut self, + _: &super::SelectWordRight, + _w: &mut Window, + cx: &mut Self::Context, + ) { + self.internal + .select_linear(NavigationDirection::Forward, TextBoundary::Word, cx); + } + + fn cut(&mut self, _: &super::Cut, _w: &mut Window, cx: &mut Self::Context) { + self.internal.cut(cx); + } + + fn copy(&mut self, _: &super::Copy, _w: &mut Window, cx: &mut Self::Context) { + self.internal.copy(cx); + } + + fn paste(&mut self, _: &super::Paste, _w: &mut Window, cx: &mut Self::Context) { + self.internal.paste(cx); + } + + fn undo(&mut self, _: &super::Undo, _w: &mut Window, _cx: &mut Self::Context) { + // TODO: STUB + } + + fn redo(&mut self, _: &super::Redo, _w: &mut Window, _cx: &mut Self::Context) { + // TODO: STUB + } + + fn on_mouse_down( + &mut self, + event: &gpui::MouseDownEvent, + text_position: gpui::Point, + window: &mut Window, + cx: &mut Self::Context, + ) { + let character_pos = self.internal.caret_pos(); // TODO: Should be index_for_pixel_point + self.internal.on_mouse_down( + text_position, + character_pos, + event.click_count, + event.modifiers.shift, + window, + cx, + ); + } + + fn on_mouse_up( + &mut self, + _event: &gpui::MouseUpEvent, + _w: &mut Window, + _cx: &mut Self::Context, + ) { + self.internal.on_mouse_up(); + } + + fn on_mouse_move( + &mut self, + _event: &gpui::MouseMoveEvent, + text_position: Point, + _w: &mut Window, + cx: &mut Self::Context, + ) { + let character_pos = self.internal.caret_pos(); // TODO: Should be index_for_pixel_point + self.internal.on_mouse_move(character_pos, cx); + } +} diff --git a/crates/gpui_elements/src/input/state.rs b/crates/gpui_elements/src/input/state.rs index ed5fbf206c..fa1ba51c34 100644 --- a/crates/gpui_elements/src/input/state.rs +++ b/crates/gpui_elements/src/input/state.rs @@ -1,9 +1,9 @@ use super::actions::*; use crate::input::{InputLayoutStyle, InputStorage}; use gpui::{ - App, AppContext, ClipboardItem, Context, Entity, EntityId, EntityInputHandler, EventEmitter, - FocusHandle, Focusable, NavigationDirection, Pixels, Point, Render, SharedString, Size, - Subscription, TextRun, TextStyle, Window, WrappedLine, point, px, + App, ClipboardItem, Context, EntityId, EntityInputHandler, EventEmitter, FocusHandle, + Focusable, NavigationDirection, Pixels, Point, SharedString, Size, TextRun, TextStyle, Window, + WrappedLine, point, px, }; use std::{ ops::Range,