outline reimplemented ime operations
This commit is contained in:
@@ -1,7 +1,16 @@
|
||||
mod actions;
|
||||
mod state_field;
|
||||
mod input_element;
|
||||
mod input_state;
|
||||
pub mod notify;
|
||||
mod shared_state;
|
||||
mod storage;
|
||||
mod text_area_element;
|
||||
mod text_area_state;
|
||||
|
||||
pub use actions::*;
|
||||
pub use state_field::*;
|
||||
pub use input_element::*;
|
||||
pub use input_state::*;
|
||||
pub use shared_state::*;
|
||||
pub use storage::*;
|
||||
pub use text_area_element::*;
|
||||
pub use text_area_state::*;
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
use gpui::ElementId;
|
||||
|
||||
pub fn input(id: impl Into<ElementId>) {}
|
||||
@@ -0,0 +1,103 @@
|
||||
use crate::editable_text::{TextInputStateBase, notify::TextChanged};
|
||||
use gpui::{
|
||||
Bounds, Context, EntityInputHandler, EventEmitter, Pixels, Point, UTF16Selection, Window,
|
||||
};
|
||||
use std::ops::Range;
|
||||
|
||||
pub struct TextInputState {
|
||||
internal: TextInputStateBase,
|
||||
}
|
||||
|
||||
impl EventEmitter<TextChanged> for TextInputState {}
|
||||
|
||||
impl EntityInputHandler for TextInputState {
|
||||
fn text_for_range(
|
||||
&mut self,
|
||||
range_utf16: Range<usize>,
|
||||
adjusted_range: &mut Option<Range<usize>>,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Self>,
|
||||
) -> Option<String> {
|
||||
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<Self>,
|
||||
) -> Option<UTF16Selection> {
|
||||
self.internal.ime_selected_text_range(ignore_disabled_input)
|
||||
}
|
||||
|
||||
fn marked_text_range(
|
||||
&self,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Self>,
|
||||
) -> Option<Range<usize>> {
|
||||
self.internal.ime_marked_text_range()
|
||||
}
|
||||
|
||||
fn unmark_text(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {
|
||||
self.internal.ime_unmark_text();
|
||||
}
|
||||
|
||||
fn replace_text_in_range(
|
||||
&mut self,
|
||||
range_utf16: Option<Range<usize>>,
|
||||
text_to_insert: &str,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let range_utf8 = self.internal.ime_resolve_range(range_utf16);
|
||||
self.internal
|
||||
.replace_text_in_range_bytes(range_utf8, text_to_insert);
|
||||
//self.mark_layout_dirty();
|
||||
//cx.emit(CursorTrigger::PauseBlinkingForUserAction);
|
||||
cx.emit(TextChanged);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn replace_and_mark_text_in_range(
|
||||
&mut self,
|
||||
range_utf16: Option<Range<usize>>,
|
||||
text_to_insert: &str,
|
||||
new_selected_range_utf16: Option<Range<usize>>,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let range = self.internal.ime_resolve_range(range_utf16);
|
||||
self.internal
|
||||
.replace_text_in_range_bytes(range.clone(), text_to_insert);
|
||||
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(TextChanged);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn bounds_for_range(
|
||||
&mut self,
|
||||
range_utf16: Range<usize>,
|
||||
bounds: Bounds<Pixels>,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Self>,
|
||||
) -> Option<Bounds<Pixels>> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn character_index_for_point(
|
||||
&mut self,
|
||||
point: Point<Pixels>,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Self>,
|
||||
) -> Option<usize> {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
use std::{ops::Range, time::Instant};
|
||||
|
||||
use crate::editable_text::UnicodeTextStorage;
|
||||
|
||||
pub struct TextChanged;
|
||||
|
||||
pub struct TextHistoryPushed {
|
||||
pub timestamp: Instant,
|
||||
pub modified_range: Range<usize>,
|
||||
pub text_payload: String,
|
||||
pub new_length: usize,
|
||||
pub selected_range: Range<usize>,
|
||||
}
|
||||
impl TextHistoryPushed {
|
||||
pub fn new(
|
||||
range: Range<usize>,
|
||||
new_length: usize,
|
||||
storage: impl UnicodeTextStorage,
|
||||
selected_range: Range<usize>,
|
||||
) -> Self {
|
||||
let timestamp = Instant::now();
|
||||
let modified_range = range.start..range.start + new_length;
|
||||
// NOTE: not performant to allocate a new text payload if the event doesnt
|
||||
// need to be logged (based on timestamp). Should consider a more robust way to access
|
||||
// the storage only if it absolutely needs to be cloned from.
|
||||
let text_payload = storage.content_utf8()[range].to_string();
|
||||
Self {
|
||||
timestamp,
|
||||
modified_range,
|
||||
text_payload,
|
||||
new_length,
|
||||
selected_range,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn convert_to_redo(self, content: &str) -> Self {
|
||||
let undo_start = self.modified_range.start;
|
||||
let undo_end = (self.modified_range.start + self.new_length).min(content.len());
|
||||
let text_payload = content[undo_start..undo_end].to_string();
|
||||
let new_length = self.text_payload.len();
|
||||
Self {
|
||||
timestamp: self.timestamp,
|
||||
modified_range: undo_start..undo_start + self.text_payload.len(),
|
||||
text_payload,
|
||||
new_length,
|
||||
selected_range: self.selected_range,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
use crate::editable_text::UnicodeTextStorage;
|
||||
use gpui::{App, FocusHandle, Focusable, NavigationDirection, Pixels, Point, UTF16Selection};
|
||||
use std::ops::Range;
|
||||
|
||||
pub struct TextInputStateBase {
|
||||
storage: Box<dyn UnicodeTextStorage>,
|
||||
|
||||
/// The utf-8 character range that is currently selected by the user.
|
||||
/// Valid both when start < end and start > end (which dictates the direction of the selection). Empty when start==end.
|
||||
/// The start of this range is always the current position of the caret (input cursor).
|
||||
///
|
||||
/// NOTE: because each input has its own selection state, its trivial for users to have multiple selections active across multiple inputs at the same time.
|
||||
/// This could be considered undesirable behavior, and could prompt the question of whether there should be a mechanism to clear selection when focus is lost.
|
||||
selected_range: Range<usize>,
|
||||
|
||||
/// The utf-8 character range of `storage` which is being composed by IME
|
||||
marked_range: Option<Range<usize>>,
|
||||
|
||||
/// True while the user is in the act of highlighting a section of the text (e.g. during mouse pressed & dragging).
|
||||
is_selecting: bool,
|
||||
/// The last ui location relative to the element that the user clicked. Used to filter when a user clicks multiple times in the same area.
|
||||
last_click_position: Option<Point<Pixels>>,
|
||||
/// The number of times the user has clicked `last_click_position`. Used to determine which click behavior to trigger, depending on single, double, or triple clicks.
|
||||
click_count: usize,
|
||||
|
||||
focus_handle: FocusHandle,
|
||||
}
|
||||
|
||||
impl Focusable for TextInputStateBase {
|
||||
fn focus_handle(&self, _: &App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl TextInputStateBase {
|
||||
/// Creates a new `Input` with the specified multiline setting.
|
||||
/// Cursor blinking is enabled by default.
|
||||
pub fn new(storage: impl Into<Box<dyn UnicodeTextStorage>>, cx: &mut App) -> Self {
|
||||
Self {
|
||||
storage: storage.into(),
|
||||
|
||||
selected_range: 0..0,
|
||||
marked_range: None,
|
||||
|
||||
is_selecting: false,
|
||||
last_click_position: None,
|
||||
click_count: 0,
|
||||
|
||||
focus_handle: cx.focus_handle(),
|
||||
}
|
||||
}
|
||||
|
||||
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> {
|
||||
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> {
|
||||
match self.selected_range.start.cmp(&self.selected_range.end) {
|
||||
std::cmp::Ordering::Less => Some(NavigationDirection::Forward),
|
||||
std::cmp::Ordering::Equal => None,
|
||||
std::cmp::Ordering::Greater => Some(NavigationDirection::Back),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn caret_pos(&self) -> usize {
|
||||
self.selected_range.start
|
||||
}
|
||||
}
|
||||
|
||||
impl TextInputStateBase {
|
||||
pub fn ime_text_for_range(
|
||||
&self,
|
||||
range_utf16: Range<usize>,
|
||||
adjusted_range: &mut Option<Range<usize>>,
|
||||
) -> Option<String> {
|
||||
let range = self.storage.utf_range_16to8(&range_utf16);
|
||||
let storage_len_utf8 = self.storage.content_utf8().len();
|
||||
let clamped_range = range.start.min(storage_len_utf8)..range.end.min(storage_len_utf8);
|
||||
adjusted_range.replace(self.storage.utf_range_8to16(&clamped_range));
|
||||
Some(self.storage.content_utf8()[clamped_range].to_string())
|
||||
}
|
||||
|
||||
pub fn ime_selected_text_range(&self, _ignore_disabled_input: bool) -> Option<UTF16Selection> {
|
||||
let selection_range = self.selected_range();
|
||||
let direction = self.selection_direction();
|
||||
Some(UTF16Selection {
|
||||
range: self.storage.utf_range_8to16(&selection_range),
|
||||
reversed: direction == Some(NavigationDirection::Back),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn ime_marked_text_range(&self) -> Option<Range<usize>> {
|
||||
self.marked_range
|
||||
.as_ref()
|
||||
.map(|range| self.storage.utf_range_8to16(range))
|
||||
}
|
||||
|
||||
pub fn ime_unmark_text(&mut self) {
|
||||
self.marked_range = None;
|
||||
}
|
||||
|
||||
pub fn ime_resolve_range(&self, range_utf16: Option<Range<usize>>) -> Range<usize> {
|
||||
// Use a series of fallbacks to pick the range to operate on.
|
||||
// Fallback order: IME provided range, active IME marked range, selection
|
||||
let range = range_utf16.map(|range_utf16| self.storage.utf_range_16to8(&range_utf16));
|
||||
let range = range.or_else(|| self.marked_range.clone());
|
||||
let range = range.unwrap_or_else(|| self.selected_range());
|
||||
|
||||
let storage_len_utf8 = self.storage().content_utf8().len();
|
||||
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) {
|
||||
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);
|
||||
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<usize>, mut text_to_insert: &str) {
|
||||
// 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
|
||||
|
||||
let max_length = None::<usize>;
|
||||
|
||||
// Decide the effective new text up front (honouring `max_length`).
|
||||
// This avoids the "apply, then truncate" path which would leave the caret past the end.
|
||||
if let Some(cap) = max_length {
|
||||
let existing_len = self.storage().content_utf8().len() - (range.end - range.start);
|
||||
let room = cap.saturating_sub(existing_len);
|
||||
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.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<usize>, text_len: usize) {
|
||||
self.marked_range = match text_len {
|
||||
0 => None,
|
||||
_ => Some(range.start..range.start + text_len),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn ime_mark_selected_range(
|
||||
&mut self,
|
||||
range_overwritten: &Range<usize>,
|
||||
new_selected_range_utf16: &Option<Range<usize>>,
|
||||
text_len: usize,
|
||||
) {
|
||||
// NOTE: Differs from yororen-ui
|
||||
// https://github.com/MeowLynxSea/yororen-ui/blob/346502ac654b77fdaff3be2d7444fca8783acfc9/crates/yororen-ui-core/src/headless/text_input_core.rs#L359-L371
|
||||
self.selected_range = {
|
||||
let new_range = new_selected_range_utf16.as_ref();
|
||||
let new_range = new_range.map(|range_utf16| self.storage.utf_range_16to8(range_utf16));
|
||||
let new_range = new_range.map(|new_range| {
|
||||
new_range.start + range_overwritten.start..new_range.end + range_overwritten.start
|
||||
});
|
||||
new_range.unwrap_or_else(|| {
|
||||
range_overwritten.start + text_len..range_overwritten.start + text_len
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
use gpui::SharedString;
|
||||
use std::ops::Range;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
@@ -9,6 +8,8 @@ pub trait UnicodeTextStorage {
|
||||
/// Returns the UTF-16 length of the content.
|
||||
fn len_utf16(&self) -> usize;
|
||||
|
||||
fn replace_range(&mut self, range: Range<usize>, text: &str);
|
||||
|
||||
fn utf_offset_8to16(&self, pos_uft8: usize) -> usize {
|
||||
// Fast path: if offset is 0, return 0
|
||||
if pos_uft8 == 0 {
|
||||
@@ -148,14 +149,8 @@ impl UnicodeTextStorage for String {
|
||||
fn len_utf16(&self) -> usize {
|
||||
self.len()
|
||||
}
|
||||
}
|
||||
|
||||
impl UnicodeTextStorage for SharedString {
|
||||
fn content_utf8(&self) -> &str {
|
||||
self.as_str()
|
||||
}
|
||||
|
||||
fn len_utf16(&self) -> usize {
|
||||
self.len()
|
||||
fn replace_range(&mut self, range: Range<usize>, text: &str) {
|
||||
self.replace_range(range, &text);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
use gpui::ElementId;
|
||||
|
||||
pub fn text_area(id: impl Into<ElementId>) {}
|
||||
Reference in New Issue
Block a user