fill out more documentation
This commit is contained in:
@@ -1,5 +1,17 @@
|
||||
//! Implementation for editable-text elements (gpui equivalent of html `<input>` and `<textarea>`).
|
||||
//! Implementation for editable-text elements (gpui equivalent of html
|
||||
//! [`<input>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input) and
|
||||
//! [`<textarea>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/textarea)).
|
||||
//!
|
||||
//! TODO: More documentation
|
||||
//! - caret blinking
|
||||
//! - history
|
||||
//! - storage
|
||||
//! - selection
|
||||
//! - ime
|
||||
//! - navigation
|
||||
//! - overflow (scroll vs clip)
|
||||
//! - auto-sizing to content via min/max w/h
|
||||
//! - mouse selection (click x2 x3 drag)
|
||||
//!
|
||||
//! Backlog of not-yet implemented features:
|
||||
//! - text sanitation & validation (see no-op implementation of [`EditableTextState::validate_incoming_text`])
|
||||
@@ -14,6 +26,7 @@ mod layout;
|
||||
mod state;
|
||||
mod storage;
|
||||
|
||||
pub use caret::*;
|
||||
pub use element::*;
|
||||
pub use state::*;
|
||||
pub use storage::*;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
//! Module containing user-input actions that are bound by EditableText elements
|
||||
use gpui::{Action, Context, InteractiveElement, WeakEntity, Window};
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
@@ -14,9 +15,9 @@ gpui::actions!(
|
||||
/// Insert a tab character at the cursor position.
|
||||
Tab,
|
||||
/// Delete the character before the cursor.
|
||||
Backspace,
|
||||
DeleteLeft,
|
||||
/// Delete the character after the cursor.
|
||||
Delete,
|
||||
DeleteRight,
|
||||
/// Delete the word before the cursor.
|
||||
DeleteWordLeft,
|
||||
/// Delete the word after the cursor.
|
||||
@@ -78,10 +79,14 @@ gpui::actions!(
|
||||
]
|
||||
);
|
||||
|
||||
/// Creates a collection of default keystroke bindings for EditableText actions.
|
||||
/// See [`ActionBindingCollection`](gpui::ActionBindingCollection) docs on how to override these bindings.
|
||||
///
|
||||
/// TODO: Collection does not supply a way to unbind a default keystroke
|
||||
pub fn default_bindings() -> gpui::ActionBindingCollection {
|
||||
let mut bindings = gpui::ActionBindingCollection::default()
|
||||
.with::<Backspace>("backspace")
|
||||
.with::<Delete>("delete")
|
||||
.with::<DeleteLeft>("backspace")
|
||||
.with::<DeleteRight>("delete")
|
||||
.with::<Tab>("tab")
|
||||
.with::<Enter>("enter")
|
||||
.with::<NavLeft>("left")
|
||||
@@ -145,47 +150,80 @@ pub fn default_bindings() -> gpui::ActionBindingCollection {
|
||||
|
||||
/// Declares stubs for all editable-text actions that an element's state entity can implement.
|
||||
pub trait EditableTextActionHandler<Context>: Sized {
|
||||
/// Blur focus from the input.
|
||||
fn escape(&mut self, _: &Escape, _w: &mut Window, _cx: &mut Context) {}
|
||||
|
||||
/// Insert a newline at the cursor position.
|
||||
fn insert_enter(&mut self, _: &Enter, _w: &mut Window, _cx: &mut Context) {}
|
||||
/// Insert a tab character at the cursor position.
|
||||
fn insert_tab(&mut self, _: &Tab, _w: &mut Window, _cx: &mut Context) {}
|
||||
|
||||
fn backspace(&mut self, _: &Backspace, _w: &mut Window, _cx: &mut Context) {}
|
||||
fn delete(&mut self, _: &Delete, _w: &mut Window, _cx: &mut Context) {}
|
||||
|
||||
/// Delete the character before the cursor.
|
||||
fn delete_left(&mut self, _: &DeleteLeft, _w: &mut Window, _cx: &mut Context) {}
|
||||
/// Delete the character after the cursor.
|
||||
fn delete_right(&mut self, _: &DeleteRight, _w: &mut Window, _cx: &mut Context) {}
|
||||
/// Delete the word before the cursor.
|
||||
fn delete_word_left(&mut self, _: &DeleteWordLeft, _w: &mut Window, _cx: &mut Context) {}
|
||||
/// Delete the word after the cursor.
|
||||
fn delete_word_right(&mut self, _: &DeleteWordRight, _w: &mut Window, _cx: &mut Context) {}
|
||||
/// Delete from the cursor to the beginning of the line.
|
||||
fn delete_to_line_start(&mut self, _: &DeleteToLineStart, _w: &mut Window, _cx: &mut Context) {}
|
||||
/// Delete from the cursor to the end of the line.
|
||||
fn delete_to_line_end(&mut self, _: &DeleteToLineEnd, _w: &mut Window, _cx: &mut Context) {}
|
||||
|
||||
/// Move the cursor one character to the left.
|
||||
fn nav_left(&mut self, _: &NavLeft, _w: &mut Window, _cx: &mut Context) {}
|
||||
/// Move the cursor one character to the right.
|
||||
fn nav_right(&mut self, _: &NavRight, _w: &mut Window, _cx: &mut Context) {}
|
||||
/// Move the cursor up one visual line.
|
||||
fn nav_up(&mut self, _: &NavUp, _w: &mut Window, _cx: &mut Context) {}
|
||||
/// Move the cursor down one visual line.
|
||||
fn nav_down(&mut self, _: &NavDown, _w: &mut Window, _cx: &mut Context) {}
|
||||
/// Move cursor to the start of the current line.
|
||||
fn nav_line_start(&mut self, _: &Home, _w: &mut Window, _cx: &mut Context) {}
|
||||
/// Move cursor to the end of the current line.
|
||||
fn nav_line_end(&mut self, _: &NavLineEnd, _w: &mut Window, _cx: &mut Context) {}
|
||||
/// Move cursor to the start of the document.
|
||||
fn nav_start(&mut self, _: &NavDocumentStart, _w: &mut Window, _cx: &mut Context) {}
|
||||
/// Move cursor to the end of the document.
|
||||
fn nav_end(&mut self, _: &NavDocumentEnd, _w: &mut Window, _cx: &mut Context) {}
|
||||
/// Move cursor one word to the left.
|
||||
fn nav_left_word(&mut self, _: &NavWordLeft, _w: &mut Window, _cx: &mut Context) {}
|
||||
/// Move cursor one word to the right.
|
||||
fn nav_right_word(&mut self, _: &NavWordRight, _w: &mut Window, _cx: &mut Context) {}
|
||||
|
||||
/// Select the entire document.
|
||||
fn select_all(&mut self, _: &SelectAll, _w: &mut Window, _cx: &mut Context) {}
|
||||
/// Extend selection one character to the left.
|
||||
fn select_left(&mut self, _: &SelectLeft, _w: &mut Window, _cx: &mut Context) {}
|
||||
/// Extend selection one character to the right.
|
||||
fn select_right(&mut self, _: &SelectRight, _w: &mut Window, _cx: &mut Context) {}
|
||||
/// Extend selection up one visual line.
|
||||
fn select_up(&mut self, _: &SelectUp, _w: &mut Window, _cx: &mut Context) {}
|
||||
/// Extend selection down one visual line.
|
||||
fn select_down(&mut self, _: &SelectDown, _w: &mut Window, _cx: &mut Context) {}
|
||||
/// Extend selection to the beginning of the document.
|
||||
fn select_start(&mut self, _: &SelectDocumentStart, _w: &mut Window, _cx: &mut Context) {}
|
||||
/// Extend selection to the end of the document.
|
||||
fn select_end(&mut self, _: &SelectDocumentEnd, _w: &mut Window, _cx: &mut Context) {}
|
||||
/// Extend selection one word to the left.
|
||||
fn select_left_word(&mut self, _: &SelectWordLeft, _w: &mut Window, _cx: &mut Context) {}
|
||||
/// Extend selection one word to the right.
|
||||
fn select_right_word(&mut self, _: &SelectWordRight, _w: &mut Window, _cx: &mut Context) {}
|
||||
|
||||
/// Cut selected text to clipboard.
|
||||
fn cut(&mut self, _: &Cut, _w: &mut Window, _cx: &mut Context) {}
|
||||
/// Copy selected text to clipboard.
|
||||
fn copy(&mut self, _: &Copy, _w: &mut Window, _cx: &mut Context) {}
|
||||
/// Paste from clipboard at the cursor position.
|
||||
fn paste(&mut self, _: &Paste, _w: &mut Window, _cx: &mut Context) {}
|
||||
|
||||
/// Undo the last edit.
|
||||
fn undo(&mut self, _: &Undo, _w: &mut Window, _cx: &mut Context) {}
|
||||
/// Redo the last undone edit.
|
||||
fn redo(&mut self, _: &Redo, _w: &mut Window, _cx: &mut Context) {}
|
||||
|
||||
/// Show the platform character palette.
|
||||
fn show_character_palette(
|
||||
&mut self,
|
||||
_: &ShowCharacterPalette,
|
||||
@@ -247,8 +285,8 @@ pub(super) trait EditableTextActionElement<State> {
|
||||
self.register_action(|state, action, window, cx| state.escape(action, window, cx));
|
||||
self.register_action(|state, action, window, cx| state.insert_enter(action, window, cx));
|
||||
self.register_action(|state, action, window, cx| state.insert_tab(action, window, cx));
|
||||
self.register_action(|state, action, window, cx| state.backspace(action, window, cx));
|
||||
self.register_action(|state, action, window, cx| state.delete(action, window, cx));
|
||||
self.register_action(|state, action, window, cx| state.delete_left(action, window, cx));
|
||||
self.register_action(|state, action, window, cx| state.delete_right(action, window, cx));
|
||||
self.register_action(|state, action, window, cx| {
|
||||
state.delete_word_left(action, window, cx)
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::time::Duration;
|
||||
use gpui::{Context, Entity, EventEmitter, Subscription};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
/// Default interval for caret blinking.
|
||||
/// Default interval for caret blinking (500ms).
|
||||
pub const DEFAULT_BLINK_INTERVAL: Duration = Duration::from_millis(500);
|
||||
|
||||
/// Events emitted that the [`Caret`] listens to.
|
||||
|
||||
@@ -14,6 +14,10 @@ use gpui::{
|
||||
use smallvec::SmallVec;
|
||||
use std::{cell::RefCell, ops::Range, rc::Rc, sync::Arc};
|
||||
|
||||
/// Creates a text input element.
|
||||
/// See [`EditableTextElement`] for usage.
|
||||
///
|
||||
/// By default it is multiline, and therefore this is semantically equivalent to [`text_area`].
|
||||
#[track_caller]
|
||||
pub fn editable_text(id: impl Into<ElementId>) -> EditableTextElement {
|
||||
let mut this = EditableTextElement {
|
||||
@@ -33,16 +37,27 @@ pub fn editable_text(id: impl Into<ElementId>) -> EditableTextElement {
|
||||
this
|
||||
}
|
||||
|
||||
/// Creates a singleline text input element.
|
||||
/// See [`EditableTextElement`] for usage.
|
||||
#[track_caller]
|
||||
pub fn text_input(id: impl Into<ElementId>) -> EditableTextElement {
|
||||
editable_text(id).multiline(false)
|
||||
}
|
||||
|
||||
/// Creates a multiline text input element.
|
||||
/// See [`EditableTextElement`] for usage.
|
||||
#[track_caller]
|
||||
pub fn text_area(id: impl Into<ElementId>) -> EditableTextElement {
|
||||
editable_text(id).multiline(true)
|
||||
}
|
||||
|
||||
/// An input field which users can type text into.
|
||||
///
|
||||
/// EditableText elements require a storage medium to be specified
|
||||
/// (defaulting to [`StringStorage`](super::StringStorage)).
|
||||
/// Use [`with_storage`](Self::with_storage) to configure the storage medium,
|
||||
/// or [`default_value`](Self::default_value) to specify the content of the default storage medium.
|
||||
///
|
||||
pub struct EditableTextElement {
|
||||
interactivity: Interactivity,
|
||||
// Populated on first render with an entity stored/attached to the view.
|
||||
@@ -86,11 +101,17 @@ impl Default for EditableTextColors {
|
||||
}
|
||||
|
||||
impl EditableTextElement {
|
||||
/// Configures whether the field supports multiple lines of text.
|
||||
/// Disabling this prevents actions like `enter` and navigating up and down.
|
||||
///
|
||||
/// It doesnt not automatically santize inputs from containing newlines (e.g. on paste).
|
||||
/// This is a limitation of the current state of implementation and requires further iteration.
|
||||
pub fn multiline(mut self, enabled: bool) -> Self {
|
||||
self.supports_multiline = enabled;
|
||||
self
|
||||
}
|
||||
|
||||
/// Assigns the text that should be displayed when storage of the element is empty.
|
||||
pub fn placeholder(mut self, text: impl Into<SharedString>) -> Self {
|
||||
self.placeholder = Some(text.into());
|
||||
self
|
||||
@@ -112,25 +133,31 @@ impl EditableTextElement {
|
||||
self
|
||||
}
|
||||
|
||||
/// Configures whether the element can accept input (effectively is the element currently enabled).
|
||||
/// Configures whether the element can accept input (effectively means "is the element currently enabled").
|
||||
pub fn accepts_input(mut self, enabled: bool) -> Self {
|
||||
self.accepts_input = enabled;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the color of the placeholder text which is rendered when the element's stored text is empty.
|
||||
///
|
||||
/// Cannot be refined via [`StyleRefinement`](gpui::StyleRefinement) due to limitations in the fields of [`Style`](gpui::Style).
|
||||
pub fn placeholder_color(mut self, color: Hsla) -> Self {
|
||||
self.colors.placeholder = color;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the color of the box highlighting selected text.
|
||||
///
|
||||
/// Cannot be refined via [`StyleRefinement`](gpui::StyleRefinement) due to limitations in the fields of [`Style`](gpui::Style).
|
||||
pub fn selection_color(mut self, color: Hsla) -> Self {
|
||||
self.colors.selection = color;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the color of the caret / text-cursor.
|
||||
///
|
||||
/// Cannot be refined via [`StyleRefinement`](gpui::StyleRefinement) due to limitations in the fields of [`Style`](gpui::Style).
|
||||
pub fn caret_color(mut self, color: Hsla) -> Self {
|
||||
self.colors.caret = color;
|
||||
self
|
||||
@@ -138,6 +165,8 @@ impl EditableTextElement {
|
||||
|
||||
/// Sets the color of the underlines rendered underneath text being editted/marked by InputMethodEditors
|
||||
/// (for writing Chinese, Japanese, and Korean utf-16).
|
||||
///
|
||||
/// Cannot be refined via [`StyleRefinement`](gpui::StyleRefinement) due to limitations in the fields of [`Style`](gpui::Style).
|
||||
pub fn marked_color(mut self, color: Hsla) -> Self {
|
||||
self.colors.ime_underline = color;
|
||||
self
|
||||
@@ -172,11 +201,6 @@ impl EditableTextActionElement<EditableTextState> for EditableTextElement {
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub struct LayoutState<State> {
|
||||
state: Entity<State>,
|
||||
}
|
||||
|
||||
struct InteractivityPrepaint {
|
||||
hitbox: Option<Hitbox>,
|
||||
scroll_offset: Point<Pixels>,
|
||||
@@ -184,6 +208,7 @@ struct InteractivityPrepaint {
|
||||
caret_visible: bool,
|
||||
}
|
||||
|
||||
/// Internal type containing prepaint information used to paint the element
|
||||
#[doc(hidden)]
|
||||
pub struct PrepaintState {
|
||||
interactivity: InteractivityPrepaint,
|
||||
@@ -192,7 +217,7 @@ pub struct PrepaintState {
|
||||
}
|
||||
|
||||
impl Element for EditableTextElement {
|
||||
type RequestLayoutState = LayoutState<EditableTextState>;
|
||||
type RequestLayoutState = Entity<EditableTextState>;
|
||||
type PrepaintState = PrepaintState;
|
||||
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
@@ -394,8 +419,7 @@ impl Element for EditableTextElement {
|
||||
},
|
||||
);
|
||||
|
||||
let layout_state = LayoutState { state };
|
||||
(layout_id, layout_state)
|
||||
(layout_id, state)
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
@@ -413,7 +437,7 @@ impl Element for EditableTextElement {
|
||||
let caret;
|
||||
let focus_handle;
|
||||
{
|
||||
let state = request_layout.state.read(cx);
|
||||
let state = request_layout.read(cx);
|
||||
content_size = state.layout_data.size.unwrap_or_else(|| bounds.size);
|
||||
caret = state.caret_entity().clone();
|
||||
focus_handle = state.focus_handle(cx);
|
||||
@@ -443,7 +467,7 @@ impl Element for EditableTextElement {
|
||||
bounds.size.height -= padding.top + padding.bottom;
|
||||
bounds
|
||||
};
|
||||
request_layout.state.update(cx, |state, _cx| {
|
||||
request_layout.update(cx, |state, _cx| {
|
||||
// while gpui tracks scroll_offset with negative values,
|
||||
// this is converted into positive for usage with bounds
|
||||
state.layout_data.scroll_bounds =
|
||||
@@ -458,7 +482,7 @@ impl Element for EditableTextElement {
|
||||
},
|
||||
);
|
||||
|
||||
let state = request_layout.state.read(cx);
|
||||
let state = request_layout.read(cx);
|
||||
let elements = PrepaintElements::build_elements(state, &prepaint, &self.colors, window);
|
||||
|
||||
PrepaintState {
|
||||
@@ -491,13 +515,12 @@ impl Element for EditableTextElement {
|
||||
}
|
||||
|
||||
if accepts_input {
|
||||
let ime_handler =
|
||||
ElementInputHandler::new(inner_bounds, request_layout.state.clone());
|
||||
let ime_handler = ElementInputHandler::new(inner_bounds, request_layout.clone());
|
||||
window.handle_input(&prepaint.focus_handle, ime_handler, cx);
|
||||
}
|
||||
|
||||
window.on_mouse_event({
|
||||
let state = request_layout.state.clone();
|
||||
let state = request_layout.clone();
|
||||
move |event: &MouseDownEvent, phase, window, cx| {
|
||||
if phase != DispatchPhase::Bubble {
|
||||
return;
|
||||
@@ -516,7 +539,7 @@ impl Element for EditableTextElement {
|
||||
}
|
||||
});
|
||||
window.on_mouse_event({
|
||||
let state = request_layout.state.clone();
|
||||
let state = request_layout.clone();
|
||||
move |event: &MouseUpEvent, phase, window, cx| {
|
||||
if phase != DispatchPhase::Bubble {
|
||||
return;
|
||||
@@ -531,7 +554,7 @@ impl Element for EditableTextElement {
|
||||
}
|
||||
});
|
||||
window.on_mouse_event({
|
||||
let state = request_layout.state.clone();
|
||||
let state = request_layout.clone();
|
||||
move |event: &MouseMoveEvent, phase, window, cx| {
|
||||
if phase != DispatchPhase::Bubble {
|
||||
return;
|
||||
|
||||
@@ -14,7 +14,7 @@ use std::{borrow::Cow, ops::Range};
|
||||
/// Event emitted via EditableText elements when the internal storage contents have changed
|
||||
pub struct TextChanged;
|
||||
|
||||
/// Internal state for EditableText elements
|
||||
/// Internal state for EditableText elements. There is no way to access this externally with the current api.
|
||||
pub struct EditableTextState {
|
||||
/// The storage medium backing this element-state. Hypothetically supports both
|
||||
/// std String and other crates (e.g. long document text).
|
||||
@@ -88,18 +88,19 @@ impl EditableTextState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the storage medium created for this field.
|
||||
pub fn storage(&self) -> &Box<dyn UnicodeTextStorage> {
|
||||
&self.storage
|
||||
}
|
||||
|
||||
/// Returns the utf-8 character range that is currently selected within the current state of the text.
|
||||
/// Internally converts the stored direction-aware range into a canonical range.
|
||||
pub fn selected_range(&self) -> Range<usize> {
|
||||
pub(super) fn selected_range(&self) -> Range<usize> {
|
||||
self.selected_range.start.min(self.selected_range.end)
|
||||
..self.selected_range.start.max(self.selected_range.end)
|
||||
}
|
||||
|
||||
pub fn selection_direction(&self) -> Option<NavigationDirection> {
|
||||
pub(super) fn selection_direction(&self) -> Option<NavigationDirection> {
|
||||
match self.selected_range.start.cmp(&self.selected_range.end) {
|
||||
std::cmp::Ordering::Less => Some(NavigationDirection::Forward),
|
||||
std::cmp::Ordering::Equal => None,
|
||||
@@ -107,18 +108,21 @@ impl EditableTextState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a reference to the entity owning the state of the [`Caret`] (e.g. its blinking state).
|
||||
pub fn caret_entity(&self) -> &Entity<Caret> {
|
||||
&self.caret
|
||||
}
|
||||
|
||||
/// Returns the position of the caret in utf8 character space.
|
||||
pub fn caret_pos(&self) -> usize {
|
||||
self.selected_range.start
|
||||
}
|
||||
|
||||
pub fn set_selected_range(&mut self, range: Range<usize>) {
|
||||
pub(super) fn set_selected_range(&mut self, range: Range<usize>) {
|
||||
self.selected_range = range;
|
||||
}
|
||||
|
||||
/// Returns the IME marked range for character operations.
|
||||
pub fn marked_range(&self) -> Option<Range<usize>> {
|
||||
self.marked_range.clone()
|
||||
}
|
||||
@@ -346,6 +350,10 @@ impl EditableTextState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Moves the caret to the provided position.
|
||||
///
|
||||
/// Will cause the current scroll position/offset to update on the next frame,
|
||||
/// if the line the carent is on is out of view.
|
||||
pub fn move_to(&mut self, caret_pos: usize, cx: &mut Context<Self>) {
|
||||
cx.emit(CaretNotify::PauseBlinking);
|
||||
let caret_pos = caret_pos.min(self.storage.content_utf8().len());
|
||||
@@ -354,6 +362,10 @@ impl EditableTextState {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Changes the current selection to extend to the provided position.
|
||||
///
|
||||
/// Will cause the current scroll position/offset to update on the next frame,
|
||||
/// if the line the carent is on is out of view.
|
||||
pub fn select_to(&mut self, caret_pos: usize, cx: &mut Context<Self>) {
|
||||
cx.emit(CaretNotify::PauseBlinking);
|
||||
let caret_pos = caret_pos.min(self.storage().content_utf8().len());
|
||||
@@ -362,6 +374,18 @@ impl EditableTextState {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Removes a chunk of text at the cursor/selection.
|
||||
/// No-op if the element is currently not accepting input.
|
||||
///
|
||||
/// If there is a selection of multiple characters, the slice of text represented
|
||||
/// by range is replaced with an empty string.
|
||||
/// If there is no selection, `direction` and `boundary` are used to determine the slice of text to remove.
|
||||
///
|
||||
/// [`NavigationDirection::Back`] represents scanning earlier in the text string from the caret.
|
||||
///
|
||||
/// [`NavigationDirection::Forward`] represents scanning later in the text string from the caret.
|
||||
///
|
||||
/// [`TextBoundary`] describes how far to jump from the caret.
|
||||
pub fn delete_linear(
|
||||
&mut self,
|
||||
direction: NavigationDirection,
|
||||
@@ -389,6 +413,15 @@ impl EditableTextState {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Moves the caret somewhere relative to its current location, according to `direction` and `boundary`.
|
||||
///
|
||||
/// If there is currently a selection, the cursor will jump to the start/end of that selection based on `direction`.
|
||||
///
|
||||
/// [`NavigationDirection::Back`] represents scanning earlier in the text string from the current caret.
|
||||
///
|
||||
/// [`NavigationDirection::Forward`] represents scanning later in the text string from the current caret.
|
||||
///
|
||||
/// [`TextBoundary`] describes how far to jump from the current caret
|
||||
pub fn nav_linear(
|
||||
&mut self,
|
||||
direction: NavigationDirection,
|
||||
@@ -407,11 +440,20 @@ impl EditableTextState {
|
||||
self.move_to(caret_pos, cx);
|
||||
}
|
||||
|
||||
/// Sets the current selection to be the entire text in the storage medium
|
||||
pub fn select_document(&mut self, cx: &mut Context<Self>) {
|
||||
self.selected_range = 0..self.storage.content_utf8().len();
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Extends the current selection to include some amount of textrelative the current
|
||||
/// location of the caret, according to `direction` and `boundary`.
|
||||
///
|
||||
/// [`NavigationDirection::Back`] represents scanning earlier in the text string from the current caret.
|
||||
///
|
||||
/// [`NavigationDirection::Forward`] represents scanning later in the text string from the current caret.
|
||||
///
|
||||
/// [`TextBoundary`] describes how far to jump from the current caret
|
||||
pub fn select_linear(
|
||||
&mut self,
|
||||
direction: NavigationDirection,
|
||||
@@ -427,6 +469,7 @@ impl EditableTextState {
|
||||
|
||||
// History management
|
||||
impl EditableTextState {
|
||||
/// Returns the history log of the element, which is the data that supports undo/redo operations.
|
||||
pub fn history(&self) -> Option<&EditableTextHistory> {
|
||||
self.history.as_ref()
|
||||
}
|
||||
@@ -679,11 +722,11 @@ impl<'app> EditableTextActionHandler<Context<'app, Self>> for EditableTextState
|
||||
self.replace_text_in_range(None, "\t", window, cx);
|
||||
}
|
||||
|
||||
fn backspace(&mut self, _: &Backspace, _: &mut Window, cx: &mut Context<'app, Self>) {
|
||||
fn delete_left(&mut self, _: &DeleteLeft, _: &mut Window, cx: &mut Context<'app, Self>) {
|
||||
self.delete_linear(NavigationDirection::Back, TextBoundary::Graphmeme, cx);
|
||||
}
|
||||
|
||||
fn delete(&mut self, _: &Delete, _w: &mut Window, cx: &mut Context<'app, Self>) {
|
||||
fn delete_right(&mut self, _: &DeleteRight, _w: &mut Window, cx: &mut Context<'app, Self>) {
|
||||
self.delete_linear(NavigationDirection::Forward, TextBoundary::Graphmeme, cx);
|
||||
}
|
||||
|
||||
@@ -1366,7 +1409,7 @@ mod tests {
|
||||
let view = create_test_input(cx, "hello world", 6..11);
|
||||
view.update(cx, |view, window, cx| {
|
||||
view.input.update(cx, |input, cx| {
|
||||
input.backspace(&Backspace, window, cx);
|
||||
input.delete_left(&DeleteLeft, window, cx);
|
||||
assert_eq!(input.storage().content_utf8(), "hello ");
|
||||
assert_eq!(input.selected_range, 6..6);
|
||||
});
|
||||
@@ -1379,7 +1422,7 @@ mod tests {
|
||||
let view = create_test_input(cx, "hello", 5..5);
|
||||
view.update(cx, |view, window, cx| {
|
||||
view.input.update(cx, |input, cx| {
|
||||
input.backspace(&Backspace, window, cx);
|
||||
input.delete_left(&DeleteLeft, window, cx);
|
||||
assert_eq!(input.storage().content_utf8(), "hell");
|
||||
assert_eq!(input.selected_range, 4..4);
|
||||
});
|
||||
@@ -1392,7 +1435,7 @@ mod tests {
|
||||
let view = create_test_input(cx, "hello", 0..0);
|
||||
view.update(cx, |view, window, cx| {
|
||||
view.input.update(cx, |input, cx| {
|
||||
input.backspace(&Backspace, window, cx);
|
||||
input.delete_left(&DeleteLeft, window, cx);
|
||||
assert_eq!(input.storage().content_utf8(), "hello");
|
||||
assert_eq!(input.selected_range, 0..0);
|
||||
});
|
||||
@@ -1405,7 +1448,7 @@ mod tests {
|
||||
let view = create_test_input(cx, "Hi 👋", 7..7);
|
||||
view.update(cx, |view, window, cx| {
|
||||
view.input.update(cx, |input, cx| {
|
||||
input.backspace(&Backspace, window, cx);
|
||||
input.delete_left(&DeleteLeft, window, cx);
|
||||
assert_eq!(input.storage().content_utf8(), "Hi ");
|
||||
assert_eq!(input.selected_range, 3..3);
|
||||
});
|
||||
@@ -1422,7 +1465,7 @@ mod tests {
|
||||
let view = create_test_input(cx, "hello world", 0..5);
|
||||
view.update(cx, |view, window, cx| {
|
||||
view.input.update(cx, |input, cx| {
|
||||
input.delete(&Delete, window, cx);
|
||||
input.delete_right(&DeleteRight, window, cx);
|
||||
assert_eq!(input.storage().content_utf8(), " world");
|
||||
assert_eq!(input.selected_range, 0..0);
|
||||
});
|
||||
@@ -1435,7 +1478,7 @@ mod tests {
|
||||
let view = create_test_input(cx, "hello", 0..0);
|
||||
view.update(cx, |view, window, cx| {
|
||||
view.input.update(cx, |input, cx| {
|
||||
input.delete(&Delete, window, cx);
|
||||
input.delete_right(&DeleteRight, window, cx);
|
||||
assert_eq!(input.storage().content_utf8(), "ello");
|
||||
assert_eq!(input.selected_range, 0..0);
|
||||
});
|
||||
@@ -1448,7 +1491,7 @@ mod tests {
|
||||
let view = create_test_input(cx, "hello", 5..5);
|
||||
view.update(cx, |view, window, cx| {
|
||||
view.input.update(cx, |input, cx| {
|
||||
input.delete(&Delete, window, cx);
|
||||
input.delete_right(&DeleteRight, window, cx);
|
||||
assert_eq!(input.storage().content_utf8(), "hello");
|
||||
assert_eq!(input.selected_range, 5..5);
|
||||
});
|
||||
@@ -1628,10 +1671,10 @@ mod tests {
|
||||
input.nav_right(&NavRight, window, cx);
|
||||
assert_eq!(input.selected_range, 0..0);
|
||||
|
||||
input.backspace(&Backspace, window, cx);
|
||||
input.delete_left(&DeleteLeft, window, cx);
|
||||
assert_eq!(input.storage().content_utf8(), "");
|
||||
|
||||
input.delete(&Delete, window, cx);
|
||||
input.delete_right(&DeleteRight, window, cx);
|
||||
assert_eq!(input.storage().content_utf8(), "");
|
||||
|
||||
input.select_all(&SelectAll, window, cx);
|
||||
@@ -1794,7 +1837,7 @@ mod tests {
|
||||
let view = create_test_input(cx, "a😀b", 5..5); // cursor after emoji
|
||||
view.update(cx, |view, window, cx| {
|
||||
view.input.update(cx, |input, cx| {
|
||||
input.backspace(&Backspace, window, cx);
|
||||
input.delete_left(&DeleteLeft, window, cx);
|
||||
assert_eq!(input.storage().content_utf8(), "ab");
|
||||
assert_eq!(input.selected_range.start, 1);
|
||||
});
|
||||
@@ -1811,7 +1854,7 @@ mod tests {
|
||||
let view = create_test_input(cx, &content, cursor_pos..cursor_pos);
|
||||
view.update(cx, |view, window, cx| {
|
||||
view.input.update(cx, |input, cx| {
|
||||
input.backspace(&Backspace, window, cx);
|
||||
input.delete_left(&DeleteLeft, window, cx);
|
||||
assert_eq!(input.storage().content_utf8(), "ab");
|
||||
assert_eq!(input.selected_range.start, 1);
|
||||
});
|
||||
@@ -1824,7 +1867,7 @@ mod tests {
|
||||
let view = create_test_input(cx, "a😀b", 1..1); // cursor before emoji
|
||||
view.update(cx, |view, window, cx| {
|
||||
view.input.update(cx, |input, cx| {
|
||||
input.delete(&Delete, window, cx);
|
||||
input.delete_right(&DeleteRight, window, cx);
|
||||
assert_eq!(input.storage().content_utf8(), "ab");
|
||||
assert_eq!(input.selected_range.start, 1);
|
||||
});
|
||||
@@ -2243,7 +2286,7 @@ mod tests {
|
||||
view.input.update(cx, |input, cx| {
|
||||
without_history_grouping(input);
|
||||
|
||||
input.backspace(&Backspace, window, cx);
|
||||
input.delete_left(&DeleteLeft, window, cx);
|
||||
assert_eq!(input.storage().content_utf8(), "hell");
|
||||
|
||||
input.undo(&Undo, window, cx);
|
||||
@@ -2260,7 +2303,7 @@ mod tests {
|
||||
view.input.update(cx, |input, cx| {
|
||||
without_history_grouping(input);
|
||||
|
||||
input.delete(&Delete, window, cx);
|
||||
input.delete_right(&DeleteRight, window, cx);
|
||||
assert_eq!(input.storage().content_utf8(), "ello");
|
||||
|
||||
input.undo(&Undo, window, cx);
|
||||
|
||||
@@ -2,6 +2,7 @@ use gpui::{App, NavigationDirection};
|
||||
use std::{ops::Range, rc::Rc};
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
/// Describes a boundary within a chunk of text.
|
||||
pub enum TextBoundary {
|
||||
/// The next utf-8 character in a direction from the caret
|
||||
Graphmeme,
|
||||
@@ -19,13 +20,15 @@ pub enum TextBoundary {
|
||||
pub struct InitStorage(Option<Rc<dyn Fn(&mut App) -> Box<dyn UnicodeTextStorage>>>);
|
||||
|
||||
impl InitStorage {
|
||||
pub fn new_generic<F>(f: F) -> Self
|
||||
/// Basic constructor which requires that the output of the func is a `dyn UnicodeTextStorage`.
|
||||
pub fn new<F>(f: F) -> Self
|
||||
where
|
||||
F: 'static + Fn(&mut App) -> Box<dyn UnicodeTextStorage>,
|
||||
{
|
||||
Self(Some(Rc::new(f)))
|
||||
}
|
||||
|
||||
/// Wrapper around [`new`] which automatically casts the output of the provided func to [`UnicodeTextStorage`].
|
||||
pub fn new_typed<F, R>(f: F) -> Self
|
||||
where
|
||||
F: 'static + Fn(&mut App) -> R,
|
||||
@@ -44,7 +47,8 @@ impl InitStorage {
|
||||
}
|
||||
}
|
||||
|
||||
/// Abstraction around any text medium that can be used as the storage for EditableText elements.
|
||||
/// Implement this trait to create a storage medium that can be used as the content of EditableText elements.
|
||||
/// Default implementation is [`StringStorage`].
|
||||
pub trait UnicodeTextStorage {
|
||||
/// Returns the version/generation of the content, which should be incremented ever time the
|
||||
/// content is changed so that rendering elements can reprocess the contents via the text layout engine.
|
||||
@@ -59,6 +63,8 @@ pub trait UnicodeTextStorage {
|
||||
/// Replace contents within the provided range with the given str slice.
|
||||
fn replace_range(&mut self, range: Range<usize>, text: &str);
|
||||
|
||||
// TODO: Refine the api for these methods
|
||||
|
||||
fn utf_offset_8to16(&self, pos_uft8: usize) -> usize {
|
||||
// Fast path: if offset is 0, return 0
|
||||
if pos_uft8 == 0 {
|
||||
@@ -241,8 +247,8 @@ pub trait UnicodeTextStorage {
|
||||
}
|
||||
}
|
||||
|
||||
/// [`UnicodeTextStorage`] implementation for std String.
|
||||
/// Not going to be the most performant, especially for large document text.
|
||||
/// [`UnicodeTextStorage`] implementation for [`String`].
|
||||
/// This is not the most performant, especially for large text documents.
|
||||
/// Its a decent default for editable text fields though.
|
||||
#[derive(Clone, Default)]
|
||||
pub struct StringStorage {
|
||||
|
||||
Reference in New Issue
Block a user