fill out more documentation

This commit is contained in:
temportalflux
2026-07-11 09:31:07 -04:00
parent cf6f785661
commit 18c80084d2
6 changed files with 175 additions and 52 deletions
+14 -1
View File
@@ -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;
+63 -20
View File
@@ -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 {