merge element and state implementations back to a single type

This commit is contained in:
temportalflux
2026-07-11 09:31:07 -04:00
parent 76a02bbc46
commit b5acea4809
8 changed files with 599 additions and 1334 deletions
+5 -13
View File
@@ -1,23 +1,17 @@
pub mod actions;
mod input_element;
mod input_state;
mod element;
pub mod notify;
mod shared_element;
mod shared_state;
mod state;
mod storage;
mod text_area_element;
mod text_area_state;
pub use input_element::*;
pub use input_state::*;
pub use shared_state::*;
pub use element::*;
pub use state::*;
pub use storage::*;
pub use text_area_element::*;
pub use text_area_state::*;
/* TODO list
- disabled input/area
- remove gpuikit based input
- auto-scroll when cursor moves
- cursor blinking
- color styling configs
- undo/redo
@@ -27,6 +21,4 @@ pub use text_area_state::*;
*/
/* Open questions:
Is is practical and worthwhile to have separate element and state implementations?
The only real difference is how some input-action handler functions are processed.
*/
@@ -1,9 +1,6 @@
use gpui::{Action, Context, InteractiveElement, WeakEntity, Window};
use std::{cell::RefCell, rc::Rc};
use gpui::{Action, AppContext, Context, InteractiveElement, WeakEntity, Window};
use crate::editable_text::StateBackedEditableText;
/// The key context used for input element keybindings.
pub const DEFAULT_INPUT_CONTEXT: &str = "Input";
@@ -146,69 +143,59 @@ pub fn default_bindings() -> gpui::ActionBindingCollection {
bindings
}
pub trait EditableTextActionHandler<'app>: Sized {
type Context: AppContext;
pub trait EditableTextActionHandler<Context>: Sized {
fn escape(&mut self, _: &Escape, _w: &mut Window, _cx: &mut Context) {}
fn escape(&mut self, _: &Escape, _w: &mut Window, _cx: &mut Self::Context) {}
fn insert_enter(&mut self, _: &Enter, _w: &mut Window, _cx: &mut Context) {}
fn insert_tab(&mut self, _: &Tab, _w: &mut Window, _cx: &mut Context) {}
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 backspace(&mut self, _: &Backspace, _w: &mut Window, _cx: &mut Context) {}
fn delete(&mut self, _: &Delete, _w: &mut Window, _cx: &mut Context) {}
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_word_left(&mut self, _: &DeleteWordLeft, _w: &mut Window, _cx: &mut Context) {}
fn delete_word_right(&mut self, _: &DeleteWordRight, _w: &mut Window, _cx: &mut Context) {}
fn delete_to_line_start(
&mut self,
_: &DeleteToBeginningOfLine,
_w: &mut Window,
_cx: &mut Self::Context,
) {
}
fn delete_to_line_end(
&mut self,
_: &DeleteToEndOfLine,
_w: &mut Window,
_cx: &mut Self::Context,
_cx: &mut Context,
) {
}
fn delete_to_line_end(&mut self, _: &DeleteToEndOfLine, _w: &mut Window, _cx: &mut Context) {}
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 nav_left(&mut self, _: &Left, _w: &mut Window, _cx: &mut Context) {}
fn nav_right(&mut self, _: &Right, _w: &mut Window, _cx: &mut Context) {}
fn nav_up(&mut self, _: &Up, _w: &mut Window, _cx: &mut Context) {}
fn nav_down(&mut self, _: &Down, _w: &mut Window, _cx: &mut Context) {}
fn nav_line_start(&mut self, _: &Home, _w: &mut Window, _cx: &mut Context) {}
fn nav_line_end(&mut self, _: &End, _w: &mut Window, _cx: &mut Context) {}
fn nav_start(&mut self, _: &MoveToBeginning, _w: &mut Window, _cx: &mut Context) {}
fn nav_end(&mut self, _: &MoveToEnd, _w: &mut Window, _cx: &mut Context) {}
fn nav_left_word(&mut self, _: &WordLeft, _w: &mut Window, _cx: &mut Context) {}
fn nav_right_word(&mut self, _: &WordRight, _w: &mut Window, _cx: &mut Context) {}
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 select_all(&mut self, _: &SelectAll, _w: &mut Window, _cx: &mut Context) {}
fn select_left(&mut self, _: &SelectLeft, _w: &mut Window, _cx: &mut Context) {}
fn select_right(&mut self, _: &SelectRight, _w: &mut Window, _cx: &mut Context) {}
fn select_up(&mut self, _: &SelectUp, _w: &mut Window, _cx: &mut Context) {}
fn select_down(&mut self, _: &SelectDown, _w: &mut Window, _cx: &mut Context) {}
fn select_start(&mut self, _: &SelectToBeginning, _w: &mut Window, _cx: &mut Context) {}
fn select_end(&mut self, _: &SelectToEnd, _w: &mut Window, _cx: &mut Context) {}
fn select_left_word(&mut self, _: &SelectWordLeft, _w: &mut Window, _cx: &mut Context) {}
fn select_right_word(&mut self, _: &SelectWordRight, _w: &mut Window, _cx: &mut Context) {}
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 cut(&mut self, _: &Cut, _w: &mut Window, _cx: &mut Context) {}
fn copy(&mut self, _: &Copy, _w: &mut Window, _cx: &mut Context) {}
fn paste(&mut self, _: &Paste, _w: &mut Window, _cx: &mut Context) {}
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 undo(&mut self, _: &Undo, _w: &mut Window, _cx: &mut Context) {}
fn redo(&mut self, _: &Redo, _w: &mut Window, _cx: &mut Context) {}
fn show_character_palette(
&mut self,
_: &ShowCharacterPalette,
window: &mut Window,
_cx: &mut Self::Context,
_cx: &mut Context,
) {
window.show_character_palette();
}
@@ -218,35 +205,30 @@ pub trait EditableTextActionHandler<'app>: Sized {
_event: &gpui::MouseDownEvent,
_text_position: gpui::Point<gpui::Pixels>,
_w: &mut Window,
_cx: &mut Self::Context,
) {
}
fn on_mouse_up(
&mut self,
_event: &gpui::MouseUpEvent,
_w: &mut Window,
_cx: &mut Self::Context,
_cx: &mut Context,
) {
}
fn on_mouse_up(&mut self, _event: &gpui::MouseUpEvent, _w: &mut Window, _cx: &mut Context) {}
fn on_mouse_move(
&mut self,
_event: &gpui::MouseMoveEvent,
_text_position: gpui::Point<gpui::Pixels>,
_w: &mut Window,
_cx: &mut Self::Context,
_cx: &mut Context,
) {
}
}
pub(super) trait EditableTextActionElement: StateBackedEditableText {
fn state_entity_rc(&self) -> &Rc<RefCell<WeakEntity<Self::State>>>;
pub(super) trait EditableTextActionElement<State> {
fn state_entity_rc(&self) -> &Rc<RefCell<WeakEntity<State>>>;
fn register_action<ActionType>(
&mut self,
listener: fn(&mut Self::State, &ActionType, &mut Window, &mut Context<Self::State>),
listener: fn(&mut State, &ActionType, &mut Window, &mut Context<State>),
) where
Self: InteractiveElement,
ActionType: Action + std::fmt::Debug,
State: 'static,
{
let entity_rc = self.state_entity_rc().clone();
self.interactivity()
@@ -263,10 +245,9 @@ pub(super) trait EditableTextActionElement: StateBackedEditableText {
fn register_actions(&mut self)
where
Self: InteractiveElement,
Self::State:
for<'app> EditableTextActionHandler<'app, Context = gpui::Context<'app, Self::State>>,
State: for<'app> EditableTextActionHandler<gpui::Context<'app, State>>,
State: 'static,
{
use super::actions::*;
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));
@@ -1,15 +1,112 @@
use crate::editable_text::{
TextInputLayoutData, TextLineSegment,
actions::{EditableTextActionElement, EditableTextActionHandler},
EditableTextState, InitStorage, TextInputLayoutData, TextLineSegment,
actions::{DEFAULT_INPUT_CONTEXT, EditableTextActionElement, EditableTextActionHandler},
};
use gpui::{
App, Bounds, Context, CursorStyle, DispatchPhase, Display, ElementInputHandler, Entity,
FocusHandle, Focusable, Hitbox, HitboxBehavior, Hsla, InteractiveElement, MouseButton,
MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, Pixels, Point, SharedString, Size,
Style, TextAlign, TextLayout, Window, WrappedLine, fill, point, px, size,
App, Bounds, CursorStyle, DispatchPhase, Display, Element, ElementId, ElementInputHandler,
Entity, FocusHandle, Focusable, Hitbox, HitboxBehavior, Hsla, InteractiveElement,
Interactivity, IntoElement, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
PaintQuad, Pixels, Point, SharedString, Size, StatefulInteractiveElement, Style,
StyleRefinement, Styled, TextAlign, TextLayout, WeakEntity, Window, WrappedLine, fill, point,
px, size,
};
use smallvec::SmallVec;
use std::{ops::Range, sync::Arc};
use std::{cell::RefCell, ops::Range, rc::Rc, sync::Arc};
#[track_caller]
pub fn editable_text(id: impl Into<ElementId>) -> EditableTextElement {
let mut this = EditableTextElement {
interactivity: Interactivity::new(),
state_entity: Rc::new(RefCell::new(WeakEntity::new_invalid())),
supports_multiline: true,
init_storage: InitStorage::default(),
placeholder: None,
};
this.interactivity.element_id = Some(id.into());
this = this.key_context(DEFAULT_INPUT_CONTEXT);
this.register_actions();
this
}
#[track_caller]
pub fn text_input(id: impl Into<ElementId>) -> EditableTextElement {
editable_text(id).multiline(false)
}
#[track_caller]
pub fn text_area(id: impl Into<ElementId>) -> EditableTextElement {
editable_text(id).multiline(true)
}
// TODO: Disabled flag/state?
pub struct EditableTextElement {
interactivity: Interactivity,
// Populated on first render with an entity stored/attached to the view.
// This reference is shared with the action handlers, which are processed between renders
// and therefore cannot otherwise access state attached to the view.
state_entity: Rc<RefCell<WeakEntity<EditableTextState>>>,
init_storage: InitStorage,
supports_multiline: bool,
placeholder: Option<SharedString>,
}
impl EditableTextElement {
pub fn multiline(mut self, enabled: bool) -> Self {
self.supports_multiline = enabled;
self
}
pub fn placeholder(mut self, text: impl Into<SharedString>) -> Self {
self.placeholder = Some(text.into());
self
}
/// Swaps the default storage container (standard String) with a custom initializer of [`UnicodeTextStorage`].
pub fn with_storage(mut self, fn_init: impl Into<InitStorage>) -> Self {
self.init_storage = fn_init.into();
self
}
/// Swaps the default storage container. The new initializer is a standard String using the provided value.
///
/// Incompatible with [`with_storage`] (they establish the same internal value).
/// If you initialize custom storage, you should be able to initialize its default value.
pub fn default_value(mut self, value: impl Into<String>) -> Self {
let storage = super::StringStorage::from(value.into());
self.init_storage = InitStorage::new_typed(move |_cx| storage.clone());
self
}
}
impl InteractiveElement for EditableTextElement {
fn interactivity(&mut self) -> &mut Interactivity {
&mut self.interactivity
}
}
// forced implementation since the API for the element doesnt use Stateful<Element>
impl StatefulInteractiveElement for EditableTextElement {}
impl Styled for EditableTextElement {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.interactivity.base_style
}
}
impl IntoElement for EditableTextElement {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
impl EditableTextActionElement<EditableTextState> for EditableTextElement {
fn state_entity_rc(&self) -> &Rc<RefCell<WeakEntity<EditableTextState>>> {
&self.state_entity
}
}
pub enum PrepaintElement {
Line {
@@ -48,23 +145,32 @@ pub struct PrepaintState {
pub caret_visible: bool,
}
pub trait EditableTextElement: InteractiveElement + EditableTextActionElement {
fn init_state(&self, cx: &mut Context<Self::State>) -> Self::State;
fn placeholder(&self) -> &Option<SharedString>;
impl Element for EditableTextElement {
type RequestLayoutState = LayoutState<EditableTextState>;
type PrepaintState = PrepaintState;
fn shared_request_layout(
fn id(&self) -> Option<ElementId> {
self.interactivity.element_id.clone()
}
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
self.interactivity.source_location()
}
fn request_layout(
&mut self,
global_id: Option<&gpui::GlobalElementId>,
inspector_id: Option<&gpui::InspectorElementId>,
window: &mut gpui::Window,
cx: &mut gpui::App,
) -> (gpui::LayoutId, LayoutState<Self::State>) {
window: &mut Window,
cx: &mut App,
) -> (gpui::LayoutId, Self::RequestLayoutState) {
// Fetches or initializes the internal state of the field
let state = match &self.interactivity().element_id {
None => unimplemented!("all input elements must be assigned an id"),
Some(element_id) => {
let state = window
.use_keyed_state(element_id.clone(), cx, |_window, cx| self.init_state(cx));
let state = window.use_keyed_state(element_id.clone(), cx, |_window, cx| {
EditableTextState::new(self.init_storage.exec(cx), cx)
});
// store a reference to the entity owned by the element for access in action handlers
*self.state_entity_rc().borrow_mut() = state.downgrade();
state
@@ -84,8 +190,9 @@ pub trait EditableTextElement: InteractiveElement + EditableTextActionElement {
// TODO: This required a gpui api change in order to sync the focus handle between Interactivity and TextInputStateBase
self.interactivity().track_focus(focus_handle);
let placeholder = self.placeholder().clone();
let placeholder = self.placeholder.clone();
let placeholder_color = Hsla::white().opacity(0.5); // TODO: as an element param
let supports_multiline = self.supports_multiline;
let layout_id = self.interactivity().request_layout(
global_id,
inspector_id,
@@ -209,6 +316,7 @@ pub trait EditableTextElement: InteractiveElement + EditableTextActionElement {
}
let layout_data = TextInputLayoutData {
supports_multiline,
wrap_width,
size: Some(size),
last_seen_storage_version,
@@ -235,15 +343,15 @@ pub trait EditableTextElement: InteractiveElement + EditableTextActionElement {
(layout_id, layout_state)
}
fn shared_prepaint(
fn prepaint(
&mut self,
global_id: Option<&gpui::GlobalElementId>,
inspector_id: Option<&gpui::InspectorElementId>,
bounds: gpui::Bounds<gpui::Pixels>,
request_layout: &mut LayoutState<Self::State>,
window: &mut gpui::Window,
cx: &mut gpui::App,
) -> PrepaintState {
bounds: Bounds<Pixels>,
request_layout: &mut Self::RequestLayoutState,
window: &mut Window,
cx: &mut App,
) -> Self::PrepaintState {
// should reflect the text content layout size of the stored text,
// so that scrolling can take it into account during prepaint.
let content_size = {
@@ -430,19 +538,16 @@ pub trait EditableTextElement: InteractiveElement + EditableTextActionElement {
}
}
fn shared_paint(
fn paint(
&mut self,
global_id: Option<&gpui::GlobalElementId>,
inspector_id: Option<&gpui::InspectorElementId>,
bounds: gpui::Bounds<gpui::Pixels>,
request_layout: &mut LayoutState<Self::State>,
prepaint: &mut PrepaintState,
window: &mut gpui::Window,
cx: &mut gpui::App,
) where
Self::State:
for<'app> EditableTextActionHandler<'app, Context = Context<'app, Self::State>>,
{
bounds: Bounds<Pixels>,
request_layout: &mut Self::RequestLayoutState,
prepaint: &mut Self::PrepaintState,
window: &mut Window,
cx: &mut App,
) {
if let Some(hitbox) = &prepaint.hitbox {
window.set_cursor_style(CursorStyle::IBeam, hitbox);
}
@@ -1,158 +0,0 @@
use crate::editable_text::{
EditableTextInputState, InitStorage, StateBackedEditableText,
actions::{DEFAULT_INPUT_CONTEXT, EditableTextActionElement},
shared_element::{self, EditableTextElement},
};
use gpui::{
App, Bounds, Element, ElementId, InteractiveElement, Interactivity, IntoElement, Pixels,
SharedString, StatefulInteractiveElement, StyleRefinement, Styled, WeakEntity, Window,
};
use std::{cell::RefCell, rc::Rc};
#[track_caller]
pub fn input(id: impl Into<ElementId>) -> TextInputElement {
let mut this = TextInputElement {
interactivity: Interactivity::new(),
state_entity: Rc::new(RefCell::new(WeakEntity::new_invalid())),
init_storage: InitStorage::default(),
placeholder: None,
};
this.interactivity.element_id = Some(id.into());
this = this.key_context(DEFAULT_INPUT_CONTEXT);
this.register_actions();
this
}
// TODO: Disabled flag/state?
pub struct TextInputElement {
interactivity: Interactivity,
// Populated on first render with an entity stored/attached to the view.
// This reference is shared with the action handlers, which are processed between renders
// and therefore cannot otherwise access state attached to the view.
state_entity: Rc<RefCell<WeakEntity<EditableTextInputState>>>,
init_storage: InitStorage,
placeholder: Option<SharedString>,
}
impl TextInputElement {
pub fn placeholder(mut self, text: impl Into<SharedString>) -> Self {
self.placeholder = Some(text.into());
self
}
/// Swaps the default storage container (standard String) with a custom initializer of [`UnicodeTextStorage`].
pub fn with_storage(mut self, fn_init: impl Into<InitStorage>) -> Self {
self.init_storage = fn_init.into();
self
}
/// Swaps the default storage container. The new initializer is a standard String using the provided value.
///
/// Incompatible with [`with_storage`] (they establish the same internal value).
/// If you initialize custom storage, you should be able to initialize its default value.
pub fn default_value(mut self, value: impl Into<String>) -> Self {
let storage = super::StringStorage::from(value.into());
self.init_storage = InitStorage::new_typed(move |_cx| storage.clone());
self
}
}
impl InteractiveElement for TextInputElement {
fn interactivity(&mut self) -> &mut Interactivity {
&mut self.interactivity
}
}
// forced implementation since the API for the element doesnt use Stateful<Element>
impl StatefulInteractiveElement for TextInputElement {}
impl Styled for TextInputElement {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.interactivity.base_style
}
}
impl IntoElement for TextInputElement {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
impl StateBackedEditableText for TextInputElement {
type State = EditableTextInputState;
}
impl EditableTextActionElement for TextInputElement {
fn state_entity_rc(&self) -> &Rc<RefCell<WeakEntity<Self::State>>> {
&self.state_entity
}
}
impl EditableTextElement for TextInputElement {
fn init_state(&self, cx: &mut gpui::prelude::Context<Self::State>) -> Self::State {
Self::State::new(self.init_storage.exec(cx), cx)
}
fn placeholder(&self) -> &Option<SharedString> {
&self.placeholder
}
}
impl Element for TextInputElement {
type RequestLayoutState = shared_element::LayoutState<EditableTextInputState>;
type PrepaintState = shared_element::PrepaintState;
fn id(&self) -> Option<ElementId> {
self.interactivity.element_id.clone()
}
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
self.interactivity.source_location()
}
fn request_layout(
&mut self,
global_id: Option<&gpui::GlobalElementId>,
inspector_id: Option<&gpui::InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (gpui::LayoutId, Self::RequestLayoutState) {
self.shared_request_layout(global_id, inspector_id, window, cx)
}
fn prepaint(
&mut self,
global_id: Option<&gpui::GlobalElementId>,
inspector_id: Option<&gpui::InspectorElementId>,
bounds: Bounds<Pixels>,
request_layout: &mut Self::RequestLayoutState,
window: &mut Window,
cx: &mut App,
) -> Self::PrepaintState {
self.shared_prepaint(global_id, inspector_id, bounds, request_layout, window, cx)
}
fn paint(
&mut self,
global_id: Option<&gpui::GlobalElementId>,
inspector_id: Option<&gpui::InspectorElementId>,
bounds: Bounds<Pixels>,
request_layout: &mut Self::RequestLayoutState,
prepaint: &mut Self::PrepaintState,
window: &mut Window,
cx: &mut App,
) {
self.shared_paint(
global_id,
inspector_id,
bounds,
request_layout,
prepaint,
window,
cx,
);
}
}
@@ -1,365 +0,0 @@
use super::notify::TextHistoryPushed;
use crate::editable_text::{
EditableTextStateBase, EditableTextStateNotifier, TextBoundary, UnicodeTextStorage,
notify::TextChanged,
};
use gpui::{
Bounds, Context, EntityInputHandler, EventEmitter, NavigationDirection, Pixels, Point,
UTF16Selection, Window,
};
use std::ops::Range;
pub struct EditableTextInputState {
internal: EditableTextStateBase,
}
impl EventEmitter<TextChanged> for EditableTextInputState {}
impl EventEmitter<TextHistoryPushed> for EditableTextInputState {}
impl std::ops::Deref for EditableTextInputState {
type Target = EditableTextStateBase;
fn deref(&self) -> &Self::Target {
&self.internal
}
}
impl std::ops::DerefMut for EditableTextInputState {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.internal
}
}
impl EditableTextInputState {
pub fn new(storage: impl Into<Box<dyn UnicodeTextStorage>>, cx: &mut Context<Self>) -> Self {
let internal = EditableTextStateBase::new(storage, cx);
Self { internal }
}
}
impl EditableTextStateNotifier for Context<'_, EditableTextInputState> {
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 EditableTextInputState {
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, cx);
//cx.emit(CursorTrigger::PauseBlinkingForUserAction);
cx.emit_text_changed(TextChanged);
cx.notify_changed();
}
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, 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(),
);
cx.emit_text_changed(TextChanged);
cx.notify_changed();
}
fn bounds_for_range(
&mut self,
range_utf16: Range<usize>,
bounds: Bounds<Pixels>,
window: &mut Window,
_cx: &mut Context<Self>,
) -> Option<Bounds<Pixels>> {
self.internal
.ime_bounds_for_range(range_utf16, bounds, window)
}
fn character_index_for_point(
&mut self,
point: Point<Pixels>,
window: &mut Window,
_cx: &mut Context<Self>,
) -> Option<usize> {
let index = self
.internal
.index_for_pixel_point(point, window.line_height());
Some(self.internal.storage().utf_offset_8to16(index))
}
}
use super::actions::*;
impl<'app> EditableTextActionHandler<'app> for EditableTextInputState {
type Context = gpui::Context<'app, Self>;
fn escape(&mut self, _: &Escape, window: &mut Window, cx: &mut Self::Context) {
self.internal.set_selected_range(0..0);
cx.notify();
window.blur();
}
fn insert_enter(&mut self, _: &Enter, _w: &mut Window, _cx: &mut Self::Context) {}
fn insert_tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Self::Context) {
self.replace_text_in_range(None, "\t", window, cx);
}
fn backspace(&mut self, _: &Backspace, _: &mut Window, cx: &mut Self::Context) {
self.internal
.delete(NavigationDirection::Back, TextBoundary::Graphmeme, cx);
}
fn delete(&mut self, _: &Delete, _w: &mut Window, cx: &mut Self::Context) {
self.internal
.delete(NavigationDirection::Forward, TextBoundary::Graphmeme, cx);
}
fn delete_word_left(&mut self, _: &DeleteWordLeft, _w: &mut Window, cx: &mut Self::Context) {
self.internal
.delete(NavigationDirection::Back, TextBoundary::Word, cx);
}
fn delete_word_right(&mut self, _: &DeleteWordRight, _w: &mut Window, cx: &mut Self::Context) {
self.internal
.delete(NavigationDirection::Forward, TextBoundary::Word, cx);
}
fn delete_to_line_start(
&mut self,
_: &DeleteToBeginningOfLine,
_w: &mut Window,
cx: &mut Self::Context,
) {
self.internal
.delete(NavigationDirection::Back, TextBoundary::Line, cx);
}
fn delete_to_line_end(
&mut self,
_: &DeleteToEndOfLine,
_w: &mut Window,
cx: &mut Self::Context,
) {
self.internal
.delete(NavigationDirection::Forward, TextBoundary::Line, cx);
}
fn nav_left(&mut self, _: &Left, _w: &mut Window, cx: &mut Self::Context) {
self.internal
.nav_linear(NavigationDirection::Back, TextBoundary::Graphmeme, cx);
}
fn nav_right(&mut self, _: &Right, _w: &mut Window, cx: &mut Self::Context) {
self.internal
.nav_linear(NavigationDirection::Forward, TextBoundary::Graphmeme, cx);
}
fn nav_up(&mut self, _: &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, _: &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, _: &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, _: &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, _: &MoveToBeginning, _w: &mut Window, cx: &mut Self::Context) {
self.internal
.nav_linear(NavigationDirection::Back, TextBoundary::Document, cx);
}
fn nav_end(&mut self, _: &MoveToEnd, _w: &mut Window, cx: &mut Self::Context) {
self.internal
.nav_linear(NavigationDirection::Forward, TextBoundary::Document, cx);
}
fn nav_left_word(&mut self, _: &WordLeft, _w: &mut Window, cx: &mut Self::Context) {
self.internal
.nav_linear(NavigationDirection::Back, TextBoundary::Word, cx);
}
fn nav_right_word(&mut self, _: &WordRight, _w: &mut Window, cx: &mut Self::Context) {
self.internal
.nav_linear(NavigationDirection::Forward, TextBoundary::Word, cx);
}
fn select_all(&mut self, _: &SelectAll, _w: &mut Window, cx: &mut Self::Context) {
self.internal.select_all(cx);
}
fn select_left(&mut self, _: &SelectLeft, _w: &mut Window, cx: &mut Self::Context) {
self.internal
.select_linear(NavigationDirection::Back, TextBoundary::Graphmeme, cx);
}
fn select_right(&mut self, _: &SelectRight, _w: &mut Window, cx: &mut Self::Context) {
self.internal
.select_linear(NavigationDirection::Forward, TextBoundary::Graphmeme, cx);
}
fn select_up(&mut self, _: &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, _: &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, _: &SelectToBeginning, _w: &mut Window, cx: &mut Self::Context) {
self.internal
.select_linear(NavigationDirection::Back, TextBoundary::Document, cx);
}
fn select_end(&mut self, _: &SelectToEnd, _w: &mut Window, cx: &mut Self::Context) {
self.internal
.select_linear(NavigationDirection::Forward, TextBoundary::Document, cx);
}
fn select_left_word(&mut self, _: &SelectWordLeft, _w: &mut Window, cx: &mut Self::Context) {
self.internal
.select_linear(NavigationDirection::Back, TextBoundary::Word, cx);
}
fn select_right_word(&mut self, _: &SelectWordRight, _w: &mut Window, cx: &mut Self::Context) {
self.internal
.select_linear(NavigationDirection::Forward, TextBoundary::Word, cx);
}
fn cut(&mut self, _: &Cut, _w: &mut Window, cx: &mut Self::Context) {
self.internal.cut(cx);
}
fn copy(&mut self, _: &Copy, _w: &mut Window, cx: &mut Self::Context) {
self.internal.copy(cx);
}
fn paste(&mut self, _: &Paste, _w: &mut Window, cx: &mut Self::Context) {
self.internal.paste(cx);
}
fn undo(&mut self, _: &Undo, _w: &mut Window, _cx: &mut Self::Context) {
// TODO: STUB
}
fn redo(&mut self, _: &Redo, _w: &mut Window, _cx: &mut Self::Context) {
// TODO: STUB
}
fn on_mouse_down(
&mut self,
event: &gpui::MouseDownEvent,
text_position: gpui::Point<gpui::Pixels>,
window: &mut Window,
cx: &mut Self::Context,
) {
let character_pos = self
.internal
.index_for_pixel_point(text_position, window.line_height());
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<Pixels>,
window: &mut Window,
cx: &mut Self::Context,
) {
let character_pos = self
.internal
.index_for_pixel_point(text_position, window.line_height());
self.internal.on_mouse_move(character_pos, cx);
}
}
@@ -4,26 +4,12 @@ use crate::editable_text::{
notify::{TextChanged, TextHistoryPushed},
};
use gpui::{
App, Bounds, ClipboardItem, EntityInputHandler, FocusHandle, Focusable, NavigationDirection,
Pixels, Point, Size, UTF16Selection, Window, WrappedLine, point,
App, Bounds, ClipboardItem, Context, EntityInputHandler, EventEmitter, FocusHandle, Focusable,
NavigationDirection, Pixels, Point, Size, UTF16Selection, Window, WrappedLine, point,
};
use std::{ops::Range, sync::Arc};
pub trait EditableTextStateNotifier {
fn notify_changed(&mut self);
fn emit_text_changed(&mut self, event: TextChanged);
fn emit_history(&mut self, event: TextHistoryPushed);
}
pub trait StateBackedEditableText {
type State: 'static
+ std::ops::Deref<Target = EditableTextStateBase>
+ std::ops::DerefMut
+ EntityInputHandler
+ for<'app> EditableTextActionHandler<'app>;
}
pub struct EditableTextStateBase {
pub struct EditableTextState {
storage: Box<dyn UnicodeTextStorage>,
/// The utf-8 character range that is currently selected by the user.
@@ -51,6 +37,7 @@ pub struct EditableTextStateBase {
#[derive(Default)]
pub(super) struct TextInputLayoutData {
pub supports_multiline: bool,
/// The last known width at which the lines were wrapped.
pub wrap_width: Option<Pixels>,
/// The last known size of the text, as generated during layout.
@@ -85,14 +72,17 @@ impl TextLineSegment {
}
}
impl Focusable for EditableTextStateBase {
impl EventEmitter<TextChanged> for EditableTextState {}
impl EventEmitter<TextHistoryPushed> for EditableTextState {}
impl Focusable for EditableTextState {
fn focus_handle(&self, _: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl EditableTextStateBase {
pub fn new(storage: impl Into<Box<dyn UnicodeTextStorage>>, cx: &mut App) -> Self {
impl EditableTextState {
pub fn new(storage: impl Into<Box<dyn UnicodeTextStorage>>, cx: &mut Context<Self>) -> Self {
Self {
storage: storage.into(),
@@ -141,7 +131,7 @@ impl EditableTextStateBase {
}
}
impl EditableTextStateBase {
impl EditableTextState {
/// Returns the utf-8 character position of the start of the line that contains the provided pixel-point.
pub fn index_for_pixel_point(&self, point: Point<Pixels>, line_height: Pixels) -> usize {
let storage_len_utf8 = self.storage.content_utf8().len();
@@ -177,39 +167,8 @@ impl EditableTextStateBase {
}
}
impl EditableTextStateBase {
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> {
impl EditableTextState {
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));
@@ -230,13 +189,8 @@ impl EditableTextStateBase {
self.selected_range = new_caret..new_caret;
}
fn emit_change_for_undo(
&self,
cx: &mut impl EditableTextStateNotifier,
range: Range<usize>,
length: usize,
) {
cx.emit_history(TextHistoryPushed::new(
fn emit_change_for_undo(&self, cx: &mut Context<Self>, range: Range<usize>, length: usize) {
cx.emit(TextHistoryPushed::new(
range.clone(),
length,
&*self.storage,
@@ -248,7 +202,7 @@ impl EditableTextStateBase {
&mut self,
range: Range<usize>,
mut text_to_insert: &str,
cx: &mut impl EditableTextStateNotifier,
cx: &mut Context<Self>,
) {
// TODO: Apply text sanitization
// single-line fields should prune \n and \r
@@ -272,14 +226,14 @@ impl EditableTextStateBase {
self.marked_range = None;
}
pub fn ime_mark_text_in_range(&mut self, range: &Range<usize>, text_len: usize) {
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(
fn ime_mark_selected_range(
&mut self,
range_overwritten: &Range<usize>,
new_selected_range_utf16: &Option<Range<usize>>,
@@ -298,84 +252,30 @@ impl EditableTextStateBase {
})
};
}
pub fn ime_bounds_for_range(
&mut self,
range_utf16: Range<usize>,
bounds: Bounds<Pixels>,
window: &mut Window,
) -> Option<Bounds<Pixels>> {
let range = self.storage.utf_range_16to8(&range_utf16);
let line_height = window.line_height();
for line in &self.layout_data.lines {
let y_offset = line.pos_y * line_height;
if line.text_range.is_empty() {
if range.start == line.text_range.start {
return Some(Bounds::from_corners(
bounds.origin + point(Pixels::ZERO, y_offset),
bounds.origin + point(gpui::px(4.), y_offset + line_height),
));
}
} else if line.text_range.contains(&range.start) {
if let Some(wrapped) = &line.wrapped_line {
let local_start = range.start - line.text_range.start;
let local_end = (range.end - line.text_range.start).min(wrapped.text.len());
let start_pos = wrapped
.position_for_index(local_start, line_height)
.unwrap_or(point(Pixels::ZERO, Pixels::ZERO));
let end_pos = wrapped
.position_for_index(local_end, line_height)
.unwrap_or_else(|| {
let last_line_y = line_height * (line.num_visual_lines - 1) as f32;
point(wrapped.width(), last_line_y)
});
let start_visual_line = (start_pos.y / line_height).floor() as usize;
let end_visual_line = (end_pos.y / line_height).floor() as usize;
if start_visual_line == end_visual_line {
return Some(Bounds::from_corners(
bounds.origin + start_pos + point(Pixels::ZERO, y_offset),
bounds.origin + point(end_pos.x, y_offset + start_pos.y + line_height),
));
} else {
return Some(Bounds::from_corners(
bounds.origin + start_pos + point(Pixels::ZERO, y_offset),
bounds.origin
+ point(wrapped.width(), y_offset + start_pos.y + line_height),
));
}
}
}
}
None
}
}
impl EditableTextStateBase {
pub fn move_to(&mut self, caret_pos: usize, cx: &mut impl EditableTextStateNotifier) {
impl EditableTextState {
pub fn move_to(&mut self, caret_pos: usize, cx: &mut Context<Self>) {
//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();
cx.notify();
}
pub fn select_to(&mut self, caret_pos: usize, cx: &mut impl EditableTextStateNotifier) {
pub fn select_to(&mut self, caret_pos: usize, cx: &mut Context<Self>) {
//cx.emit(CursorTrigger::PauseBlinkingForUserAction);
let caret_pos = caret_pos.min(self.storage().content_utf8().len());
self.selected_range.start = caret_pos;
//self.scroll_to_cursor();
cx.notify_changed();
cx.notify();
}
pub fn delete(
pub fn delete_linear(
&mut self,
direction: NavigationDirection,
boundary: TextBoundary,
cx: &mut impl EditableTextStateNotifier,
cx: &mut Context<Self>,
) {
let range = self.selected_range();
let range = match range.is_empty() {
@@ -390,15 +290,15 @@ impl EditableTextStateBase {
self.replace_text(&range, "");
self.marked_range = None;
cx.emit_text_changed(TextChanged);
cx.notify_changed();
cx.emit(TextChanged);
cx.notify();
}
pub fn nav_linear(
&mut self,
direction: NavigationDirection,
boundary: TextBoundary,
cx: &mut impl EditableTextStateNotifier,
cx: &mut Context<Self>,
) {
let caret_pos = match self.selected_range.is_empty() {
false => match direction {
@@ -488,27 +388,359 @@ impl EditableTextStateBase {
(direction > 0).then(|| self.storage.content_utf8().len())
}
pub fn select_all(&mut self, cx: &mut impl EditableTextStateNotifier) {
pub fn select_document(&mut self, cx: &mut Context<Self>) {
self.selected_range = 0..self.storage.content_utf8().len();
cx.notify_changed();
cx.notify();
}
pub fn select_linear(
&mut self,
direction: NavigationDirection,
boundary: TextBoundary,
cx: &mut impl EditableTextStateNotifier,
cx: &mut Context<Self>,
) {
let caret_pos = self
.storage
.offset_from_caret(self.caret_pos(), direction, boundary);
self.select_to(caret_pos, cx);
}
}
pub fn cut<T>(&mut self, cx: &mut T)
where
T: EditableTextStateNotifier + std::ops::Deref<Target = App>,
{
impl EntityInputHandler for EditableTextState {
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> {
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())
}
fn selected_text_range(
&mut self,
_ignore_disabled_input: bool,
_window: &mut Window,
_cx: &mut Context<Self>,
) -> 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),
})
}
fn marked_text_range(
&self,
_window: &mut Window,
_cx: &mut Context<Self>,
) -> Option<Range<usize>> {
self.marked_range
.as_ref()
.map(|range| self.storage.utf_range_8to16(range))
}
fn unmark_text(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {
self.marked_range = None;
}
fn replace_text_in_range(
&mut self,
range_utf16: Option<Range<usize>>,
text_to_insert: &str,
_window: &mut Window,
cx: &mut Context<Self>,
) {
let range_utf8 = self.ime_resolve_range(range_utf16);
self.replace_text_in_range_bytes(range_utf8, text_to_insert, cx);
//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.ime_resolve_range(range_utf16);
self.replace_text_in_range_bytes(range.clone(), text_to_insert, cx);
self.ime_mark_text_in_range(&range, text_to_insert.len());
self.ime_mark_selected_range(&range, &new_selected_range_utf16, text_to_insert.len());
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>> {
let range = self.storage.utf_range_16to8(&range_utf16);
let line_height = window.line_height();
for line in &self.layout_data.lines {
let y_offset = line.pos_y * line_height;
if line.text_range.is_empty() {
if range.start == line.text_range.start {
return Some(Bounds::from_corners(
bounds.origin + point(Pixels::ZERO, y_offset),
bounds.origin + point(gpui::px(4.), y_offset + line_height),
));
}
} else if line.text_range.contains(&range.start) {
if let Some(wrapped) = &line.wrapped_line {
let local_start = range.start - line.text_range.start;
let local_end = (range.end - line.text_range.start).min(wrapped.text.len());
let start_pos = wrapped
.position_for_index(local_start, line_height)
.unwrap_or(point(Pixels::ZERO, Pixels::ZERO));
let end_pos = wrapped
.position_for_index(local_end, line_height)
.unwrap_or_else(|| {
let last_line_y = line_height * (line.num_visual_lines - 1) as f32;
point(wrapped.width(), last_line_y)
});
let start_visual_line = (start_pos.y / line_height).floor() as usize;
let end_visual_line = (end_pos.y / line_height).floor() as usize;
if start_visual_line == end_visual_line {
return Some(Bounds::from_corners(
bounds.origin + start_pos + point(Pixels::ZERO, y_offset),
bounds.origin + point(end_pos.x, y_offset + start_pos.y + line_height),
));
} else {
return Some(Bounds::from_corners(
bounds.origin + start_pos + point(Pixels::ZERO, y_offset),
bounds.origin
+ point(wrapped.width(), y_offset + start_pos.y + line_height),
));
}
}
}
}
None
}
fn character_index_for_point(
&mut self,
point: Point<Pixels>,
window: &mut Window,
_cx: &mut Context<Self>,
) -> Option<usize> {
let index = self.index_for_pixel_point(point, window.line_height());
Some(self.storage().utf_offset_8to16(index))
}
}
use super::actions::*;
impl<'app> EditableTextActionHandler<Context<'app, Self>> for EditableTextState {
fn escape(&mut self, _: &Escape, window: &mut Window, cx: &mut Context<'app, Self>) {
self.set_selected_range(0..0);
cx.notify();
window.blur();
}
fn insert_enter(&mut self, _: &Enter, window: &mut Window, cx: &mut Context<'app, Self>) {
if self.layout_data.supports_multiline {
// TODO: Why is the cursor appearing at the start of the entire field instead of on the new line?
self.replace_text_in_range(None, "\n", window, cx);
}
}
fn insert_tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context<'app, Self>) {
self.replace_text_in_range(None, "\t", window, cx);
}
fn backspace(&mut self, _: &Backspace, _: &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>) {
self.delete_linear(NavigationDirection::Forward, TextBoundary::Graphmeme, cx);
}
fn delete_word_left(
&mut self,
_: &DeleteWordLeft,
_w: &mut Window,
cx: &mut Context<'app, Self>,
) {
self.delete_linear(NavigationDirection::Back, TextBoundary::Word, cx);
}
fn delete_word_right(
&mut self,
_: &DeleteWordRight,
_w: &mut Window,
cx: &mut Context<'app, Self>,
) {
self.delete_linear(NavigationDirection::Forward, TextBoundary::Word, cx);
}
fn delete_to_line_start(
&mut self,
_: &DeleteToBeginningOfLine,
_w: &mut Window,
cx: &mut Context<'app, Self>,
) {
self.delete_linear(NavigationDirection::Back, TextBoundary::Line, cx);
}
fn delete_to_line_end(
&mut self,
_: &DeleteToEndOfLine,
_w: &mut Window,
cx: &mut Context<'app, Self>,
) {
self.delete_linear(NavigationDirection::Forward, TextBoundary::Line, cx);
}
fn nav_left(&mut self, _: &Left, _w: &mut Window, cx: &mut Context<'app, Self>) {
self.nav_linear(NavigationDirection::Back, TextBoundary::Graphmeme, cx);
}
fn nav_right(&mut self, _: &Right, _w: &mut Window, cx: &mut Context<'app, Self>) {
self.nav_linear(NavigationDirection::Forward, TextBoundary::Graphmeme, cx);
}
fn nav_up(&mut self, _: &Up, window: &mut Window, cx: &mut Context<'app, Self>) {
if !self.layout_data.supports_multiline {
// semantically equivalent to line
self.nav_linear(NavigationDirection::Back, TextBoundary::Line, cx);
return;
}
if let Some(caret_pos) = self.find_position_in_vertical_direction(-1, window.line_height())
{
self.move_to(caret_pos, cx);
}
}
fn nav_down(&mut self, _: &Down, window: &mut Window, cx: &mut Context<'app, Self>) {
if !self.layout_data.supports_multiline {
// semantically equivalent to line
self.nav_linear(NavigationDirection::Forward, TextBoundary::Line, cx);
return;
}
if let Some(caret_pos) = self.find_position_in_vertical_direction(1, window.line_height()) {
self.move_to(caret_pos, cx);
}
}
fn nav_line_start(&mut self, _: &Home, _w: &mut Window, cx: &mut Context<'app, Self>) {
// [when not multiline] semantically equivalent to document
self.nav_linear(NavigationDirection::Back, TextBoundary::Line, cx);
}
fn nav_line_end(&mut self, _: &End, _w: &mut Window, cx: &mut Context<'app, Self>) {
// [when not multiline] semantically equivalent to document
self.nav_linear(NavigationDirection::Forward, TextBoundary::Line, cx);
}
fn nav_start(&mut self, _: &MoveToBeginning, _w: &mut Window, cx: &mut Context<'app, Self>) {
self.nav_linear(NavigationDirection::Back, TextBoundary::Document, cx);
}
fn nav_end(&mut self, _: &MoveToEnd, _w: &mut Window, cx: &mut Context<'app, Self>) {
self.nav_linear(NavigationDirection::Forward, TextBoundary::Document, cx);
}
fn nav_left_word(&mut self, _: &WordLeft, _w: &mut Window, cx: &mut Context<'app, Self>) {
self.nav_linear(NavigationDirection::Back, TextBoundary::Word, cx);
}
fn nav_right_word(&mut self, _: &WordRight, _w: &mut Window, cx: &mut Context<'app, Self>) {
self.nav_linear(NavigationDirection::Forward, TextBoundary::Word, cx);
}
fn select_all(&mut self, _: &SelectAll, _w: &mut Window, cx: &mut Context<'app, Self>) {
self.select_document(cx);
}
fn select_left(&mut self, _: &SelectLeft, _w: &mut Window, cx: &mut Context<'app, Self>) {
self.select_linear(NavigationDirection::Back, TextBoundary::Graphmeme, cx);
}
fn select_right(&mut self, _: &SelectRight, _w: &mut Window, cx: &mut Context<'app, Self>) {
self.select_linear(NavigationDirection::Forward, TextBoundary::Graphmeme, cx);
}
fn select_up(&mut self, _: &SelectUp, window: &mut Window, cx: &mut Context<'app, Self>) {
if !self.layout_data.supports_multiline {
// semantically equivalent to select document
self.select_linear(NavigationDirection::Back, TextBoundary::Document, cx);
return;
}
if let Some(caret_pos) = self.find_position_in_vertical_direction(-1, window.line_height())
{
self.select_to(caret_pos, cx);
//self.scroll_to_cursor();
cx.notify();
}
}
fn select_down(&mut self, _: &SelectDown, window: &mut Window, cx: &mut Context<'app, Self>) {
if !self.layout_data.supports_multiline {
// semantically equivalent to select document
self.select_linear(NavigationDirection::Forward, TextBoundary::Document, cx);
return;
}
if let Some(caret_pos) = self.find_position_in_vertical_direction(1, window.line_height()) {
self.select_to(caret_pos, cx);
//self.scroll_to_cursor();
cx.notify();
}
}
fn select_start(
&mut self,
_: &SelectToBeginning,
_w: &mut Window,
cx: &mut Context<'app, Self>,
) {
self.select_linear(NavigationDirection::Back, TextBoundary::Document, cx);
}
fn select_end(&mut self, _: &SelectToEnd, _w: &mut Window, cx: &mut Context<'app, Self>) {
self.select_linear(NavigationDirection::Forward, TextBoundary::Document, cx);
}
fn select_left_word(
&mut self,
_: &SelectWordLeft,
_w: &mut Window,
cx: &mut Context<'app, Self>,
) {
self.select_linear(NavigationDirection::Back, TextBoundary::Word, cx);
}
fn select_right_word(
&mut self,
_: &SelectWordRight,
_w: &mut Window,
cx: &mut Context<'app, Self>,
) {
self.select_linear(NavigationDirection::Forward, TextBoundary::Word, cx);
}
fn cut(&mut self, _: &Cut, _w: &mut Window, cx: &mut Context<'app, Self>) {
if !self.selected_range.is_empty() {
// Cut selected text
let slice = &self.storage.content_utf8()[self.selected_range.clone()];
@@ -545,40 +777,43 @@ impl EditableTextStateBase {
self.replace_text_in_range_bytes(self.selected_range.clone(), "", cx);
}
cx.emit_text_changed(TextChanged);
cx.notify_changed();
cx.emit(TextChanged);
cx.notify();
}
pub fn copy(&mut self, app: &mut App) {
fn copy(&mut self, _: &Copy, _w: &mut Window, cx: &mut Context<'app, Self>) {
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()));
cx.write_to_clipboard(ClipboardItem::new_string(slice.to_string()));
}
}
pub fn paste<T>(&mut self, cx: &mut T)
where
T: EditableTextStateNotifier + std::ops::Deref<Target = App>,
{
fn paste(&mut self, _: &Paste, _w: &mut Window, cx: &mut Context<'app, Self>) {
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();
cx.emit(TextChanged);
cx.notify();
}
pub fn on_mouse_down<Context>(
fn undo(&mut self, _: &Undo, _w: &mut Window, _cx: &mut Context<'app, Self>) {
// TODO: STUB
}
fn redo(&mut self, _: &Redo, _w: &mut Window, _cx: &mut Context<'app, Self>) {
// TODO: STUB
}
fn on_mouse_down(
&mut self,
position: Point<Pixels>,
character_pos: usize,
click_count: usize,
shift: bool,
event: &gpui::MouseDownEvent,
text_position: gpui::Point<gpui::Pixels>,
window: &mut Window,
cx: &mut Context,
) where
Context: EditableTextStateNotifier + std::ops::DerefMut<Target = App>,
{
cx: &mut Context<'app, Self>,
) {
let character_pos = self.index_for_pixel_point(text_position, window.line_height());
window.focus(&self.focus_handle, cx);
self.is_selecting = true;
@@ -586,22 +821,23 @@ impl EditableTextStateBase {
.last_click_position
.map(|last| {
let threshold = gpui::px(4.);
(position.x - last.x).abs() < threshold && (position.y - last.y).abs() < threshold
(text_position.x - last.x).abs() < threshold
&& (text_position.y - last.y).abs() < threshold
})
.unwrap_or(false);
if is_same_position && click_count > 1 {
self.click_count = click_count;
if is_same_position && event.click_count > 1 {
self.click_count = event.click_count;
} else {
self.click_count = 1;
}
self.last_click_position = Some(position);
self.last_click_position = Some(text_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_changed();
cx.notify();
}
3 => {
let line_start = self.storage.find_line_start(character_pos);
@@ -612,10 +848,10 @@ impl EditableTextStateBase {
line_end
};
self.selected_range = line_start..line_end_with_newline;
cx.notify_changed();
cx.notify();
}
_ => {
if shift {
if event.modifiers.shift {
self.select_to(character_pos, cx);
} else {
self.move_to(character_pos, cx);
@@ -624,11 +860,23 @@ impl EditableTextStateBase {
}
}
pub fn on_mouse_up(&mut self) {
fn on_mouse_up(
&mut self,
_event: &gpui::MouseUpEvent,
_w: &mut Window,
_cx: &mut Context<'app, Self>,
) {
self.is_selecting = false;
}
pub fn on_mouse_move(&mut self, character_pos: usize, cx: &mut impl EditableTextStateNotifier) {
fn on_mouse_move(
&mut self,
_event: &gpui::MouseMoveEvent,
text_position: Point<Pixels>,
window: &mut Window,
cx: &mut Context<'app, Self>,
) {
let character_pos = self.index_for_pixel_point(text_position, window.line_height());
if self.is_selecting && self.click_count == 1 {
self.select_to(character_pos, cx);
}
@@ -1,157 +0,0 @@
use crate::editable_text::{
EditableTextAreaState, InitStorage, StateBackedEditableText,
actions::{DEFAULT_INPUT_CONTEXT, EditableTextActionElement},
shared_element::{self, EditableTextElement},
};
use gpui::{
App, Bounds, Element, ElementId, InteractiveElement, Interactivity, IntoElement, Pixels,
SharedString, StatefulInteractiveElement, StyleRefinement, Styled, WeakEntity, Window,
};
use std::{cell::RefCell, rc::Rc};
pub fn text_area(id: impl Into<ElementId>) -> EditableTextAreaElement {
let mut this = EditableTextAreaElement {
interactivity: Interactivity::new(),
state_entity: Rc::new(RefCell::new(WeakEntity::new_invalid())),
init_storage: InitStorage::default(),
placeholder: None,
};
this.interactivity.element_id = Some(id.into());
this = this.key_context(DEFAULT_INPUT_CONTEXT);
this.register_actions();
this
}
// TODO: Disabled flag/state?
pub struct EditableTextAreaElement {
interactivity: Interactivity,
// Populated on first render with an entity stored/attached to the view.
// This reference is shared with the action handlers, which are processed between renders
// and therefore cannot otherwise access state attached to the view.
state_entity: Rc<RefCell<WeakEntity<EditableTextAreaState>>>,
init_storage: InitStorage,
placeholder: Option<SharedString>,
}
impl EditableTextAreaElement {
pub fn placeholder(mut self, text: impl Into<SharedString>) -> Self {
self.placeholder = Some(text.into());
self
}
/// Swaps the default storage container (standard String) with a custom initializer of [`UnicodeTextStorage`].
pub fn with_storage(mut self, fn_init: impl Into<InitStorage>) -> Self {
self.init_storage = fn_init.into();
self
}
/// Swaps the default storage container. The new initializer is a standard String using the provided value.
///
/// Incompatible with [`with_storage`] (they establish the same internal value).
/// If you initialize custom storage, you should be able to initialize its default value.
pub fn default_value(mut self, value: impl Into<String>) -> Self {
let storage = super::StringStorage::from(value.into());
self.init_storage = InitStorage::new_typed(move |_cx| storage.clone());
self
}
}
impl InteractiveElement for EditableTextAreaElement {
fn interactivity(&mut self) -> &mut Interactivity {
&mut self.interactivity
}
}
// forced implementation since the API for the element doesnt use Stateful<Element>
impl StatefulInteractiveElement for EditableTextAreaElement {}
impl Styled for EditableTextAreaElement {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.interactivity.base_style
}
}
impl IntoElement for EditableTextAreaElement {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
impl StateBackedEditableText for EditableTextAreaElement {
type State = EditableTextAreaState;
}
impl EditableTextActionElement for EditableTextAreaElement {
fn state_entity_rc(&self) -> &Rc<RefCell<WeakEntity<Self::State>>> {
&self.state_entity
}
}
impl EditableTextElement for EditableTextAreaElement {
fn init_state(&self, cx: &mut gpui::prelude::Context<Self::State>) -> Self::State {
Self::State::new(self.init_storage.exec(cx), cx)
}
fn placeholder(&self) -> &Option<SharedString> {
&self.placeholder
}
}
impl Element for EditableTextAreaElement {
type RequestLayoutState = shared_element::LayoutState<EditableTextAreaState>;
type PrepaintState = shared_element::PrepaintState;
fn id(&self) -> Option<ElementId> {
self.interactivity.element_id.clone()
}
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
self.interactivity.source_location()
}
fn request_layout(
&mut self,
global_id: Option<&gpui::GlobalElementId>,
inspector_id: Option<&gpui::InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (gpui::LayoutId, Self::RequestLayoutState) {
self.shared_request_layout(global_id, inspector_id, window, cx)
}
fn prepaint(
&mut self,
global_id: Option<&gpui::GlobalElementId>,
inspector_id: Option<&gpui::InspectorElementId>,
bounds: Bounds<Pixels>,
request_layout: &mut Self::RequestLayoutState,
window: &mut Window,
cx: &mut App,
) -> Self::PrepaintState {
self.shared_prepaint(global_id, inspector_id, bounds, request_layout, window, cx)
}
fn paint(
&mut self,
global_id: Option<&gpui::GlobalElementId>,
inspector_id: Option<&gpui::InspectorElementId>,
bounds: Bounds<Pixels>,
request_layout: &mut Self::RequestLayoutState,
prepaint: &mut Self::PrepaintState,
window: &mut Window,
cx: &mut App,
) {
self.shared_paint(
global_id,
inspector_id,
bounds,
request_layout,
prepaint,
window,
cx,
);
}
}
@@ -1,381 +0,0 @@
use crate::editable_text::{
EditableTextStateBase, EditableTextStateNotifier, TextBoundary, UnicodeTextStorage,
notify::{TextChanged, TextHistoryPushed},
};
use gpui::{
Bounds, Context, EntityInputHandler, EventEmitter, NavigationDirection, Pixels, Point,
UTF16Selection, Window,
};
use std::ops::Range;
pub struct EditableTextAreaState {
internal: EditableTextStateBase,
}
impl EventEmitter<TextChanged> for EditableTextAreaState {}
impl EventEmitter<TextHistoryPushed> for EditableTextAreaState {}
impl std::ops::Deref for EditableTextAreaState {
type Target = EditableTextStateBase;
fn deref(&self) -> &Self::Target {
&self.internal
}
}
impl std::ops::DerefMut for EditableTextAreaState {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.internal
}
}
impl EditableTextAreaState {
pub fn new(storage: impl Into<Box<dyn UnicodeTextStorage>>, cx: &mut Context<Self>) -> Self {
let internal = EditableTextStateBase::new(storage, cx);
Self { internal }
}
}
impl EditableTextStateNotifier for Context<'_, EditableTextAreaState> {
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 EditableTextAreaState {
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, cx);
//cx.emit(CursorTrigger::PauseBlinkingForUserAction);
cx.emit_text_changed(TextChanged);
cx.notify_changed();
}
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, 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(),
);
cx.emit_text_changed(TextChanged);
cx.notify_changed();
}
fn bounds_for_range(
&mut self,
range_utf16: Range<usize>,
bounds: Bounds<Pixels>,
window: &mut Window,
_cx: &mut Context<Self>,
) -> Option<Bounds<Pixels>> {
self.internal
.ime_bounds_for_range(range_utf16, bounds, window)
}
fn character_index_for_point(
&mut self,
point: Point<Pixels>,
window: &mut Window,
_cx: &mut Context<Self>,
) -> Option<usize> {
let index = self
.internal
.index_for_pixel_point(point, window.line_height());
Some(self.internal.storage().utf_offset_8to16(index))
}
}
use super::actions::*;
impl<'app> EditableTextActionHandler<'app> for EditableTextAreaState {
type Context = gpui::Context<'app, Self>;
fn escape(&mut self, _: &Escape, window: &mut Window, cx: &mut Self::Context) {
self.internal.set_selected_range(0..0);
cx.notify();
window.blur();
}
fn insert_enter(&mut self, _: &Enter, window: &mut Window, cx: &mut Self::Context) {
// TODO: Why is the cursor appearing at the start of the entire field instead of on the new line?
self.replace_text_in_range(None, "\n", window, cx);
}
fn insert_tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Self::Context) {
self.replace_text_in_range(None, "\t", window, cx);
}
fn backspace(&mut self, _: &Backspace, _w: &mut Window, cx: &mut Self::Context) {
self.internal
.delete(NavigationDirection::Back, TextBoundary::Graphmeme, cx);
}
fn delete(&mut self, _: &Delete, _w: &mut Window, cx: &mut Self::Context) {
self.internal
.delete(NavigationDirection::Forward, TextBoundary::Graphmeme, cx);
}
fn delete_word_left(&mut self, _: &DeleteWordLeft, _w: &mut Window, cx: &mut Self::Context) {
self.internal
.delete(NavigationDirection::Back, TextBoundary::Word, cx);
}
fn delete_word_right(&mut self, _: &DeleteWordRight, _w: &mut Window, cx: &mut Self::Context) {
self.internal
.delete(NavigationDirection::Forward, TextBoundary::Word, cx);
}
fn delete_to_line_start(
&mut self,
_: &DeleteToBeginningOfLine,
_w: &mut Window,
cx: &mut Self::Context,
) {
self.internal
.delete(NavigationDirection::Back, TextBoundary::Line, cx);
}
fn delete_to_line_end(
&mut self,
_: &DeleteToEndOfLine,
_w: &mut Window,
cx: &mut Self::Context,
) {
self.internal
.delete(NavigationDirection::Forward, TextBoundary::Line, cx);
}
fn nav_left(&mut self, _: &Left, _w: &mut Window, cx: &mut Self::Context) {
self.internal
.nav_linear(NavigationDirection::Back, TextBoundary::Graphmeme, cx);
}
fn nav_right(&mut self, _: &Right, _w: &mut Window, cx: &mut Self::Context) {
self.internal
.nav_linear(NavigationDirection::Forward, TextBoundary::Graphmeme, cx);
}
fn nav_up(&mut self, _: &Up, window: &mut Window, cx: &mut Self::Context) {
if let Some(caret_pos) = self
.internal
.find_position_in_vertical_direction(-1, window.line_height())
{
self.internal.move_to(caret_pos, cx);
}
}
fn nav_down(&mut self, _: &Down, window: &mut Window, cx: &mut Self::Context) {
if let Some(caret_pos) = self
.internal
.find_position_in_vertical_direction(1, window.line_height())
{
self.internal.move_to(caret_pos, cx);
}
}
fn nav_line_start(&mut self, _: &Home, _w: &mut Window, cx: &mut Self::Context) {
self.internal
.nav_linear(NavigationDirection::Back, TextBoundary::Line, cx);
}
fn nav_line_end(&mut self, _: &End, _w: &mut Window, cx: &mut Self::Context) {
self.internal
.nav_linear(NavigationDirection::Forward, TextBoundary::Line, cx);
}
fn nav_start(&mut self, _: &MoveToBeginning, _w: &mut Window, cx: &mut Self::Context) {
self.internal
.nav_linear(NavigationDirection::Back, TextBoundary::Document, cx);
}
fn nav_end(&mut self, _: &MoveToEnd, _w: &mut Window, cx: &mut Self::Context) {
self.internal
.nav_linear(NavigationDirection::Forward, TextBoundary::Document, cx);
}
fn nav_left_word(&mut self, _: &WordLeft, _w: &mut Window, cx: &mut Self::Context) {
self.internal
.nav_linear(NavigationDirection::Back, TextBoundary::Word, cx);
}
fn nav_right_word(&mut self, _: &WordRight, _w: &mut Window, cx: &mut Self::Context) {
self.internal
.nav_linear(NavigationDirection::Forward, TextBoundary::Word, cx);
}
fn select_all(&mut self, _: &SelectAll, _w: &mut Window, cx: &mut Self::Context) {
self.internal.select_all(cx);
}
fn select_left(&mut self, _: &SelectLeft, _w: &mut Window, cx: &mut Self::Context) {
self.internal
.select_linear(NavigationDirection::Back, TextBoundary::Graphmeme, cx);
}
fn select_right(&mut self, _: &SelectRight, _w: &mut Window, cx: &mut Self::Context) {
self.internal
.select_linear(NavigationDirection::Forward, TextBoundary::Graphmeme, cx);
}
fn select_up(&mut self, _: &SelectUp, window: &mut Window, cx: &mut Self::Context) {
if let Some(caret_pos) = self
.internal
.find_position_in_vertical_direction(-1, window.line_height())
{
self.internal.select_to(caret_pos, cx);
//self.scroll_to_cursor();
cx.notify_changed();
}
}
fn select_down(&mut self, _: &SelectDown, window: &mut Window, cx: &mut Self::Context) {
if let Some(caret_pos) = self
.internal
.find_position_in_vertical_direction(1, window.line_height())
{
self.internal.select_to(caret_pos, cx);
//self.scroll_to_cursor();
cx.notify_changed();
}
}
fn select_start(&mut self, _: &SelectToBeginning, _w: &mut Window, cx: &mut Self::Context) {
self.internal
.select_linear(NavigationDirection::Back, TextBoundary::Document, cx);
}
fn select_end(&mut self, _: &SelectToEnd, _w: &mut Window, cx: &mut Self::Context) {
self.internal
.select_linear(NavigationDirection::Forward, TextBoundary::Document, cx);
}
fn select_left_word(&mut self, _: &SelectWordLeft, _w: &mut Window, cx: &mut Self::Context) {
self.internal
.select_linear(NavigationDirection::Back, TextBoundary::Word, cx);
}
fn select_right_word(&mut self, _: &SelectWordRight, _w: &mut Window, cx: &mut Self::Context) {
self.internal
.select_linear(NavigationDirection::Forward, TextBoundary::Word, cx);
}
fn cut(&mut self, _: &Cut, _w: &mut Window, cx: &mut Self::Context) {
self.internal.cut(cx);
}
fn copy(&mut self, _: &Copy, _w: &mut Window, cx: &mut Self::Context) {
self.internal.copy(cx);
}
fn paste(&mut self, _: &Paste, _w: &mut Window, cx: &mut Self::Context) {
self.internal.paste(cx);
}
fn undo(&mut self, _: &Undo, _w: &mut Window, _cx: &mut Self::Context) {
// TODO: STUB
}
fn redo(&mut self, _: &Redo, _w: &mut Window, _cx: &mut Self::Context) {
// TODO: STUB
}
fn on_mouse_down(
&mut self,
event: &gpui::MouseDownEvent,
text_position: gpui::Point<gpui::Pixels>,
window: &mut Window,
cx: &mut Self::Context,
) {
let character_pos = self
.internal
.index_for_pixel_point(text_position, window.line_height());
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<Pixels>,
window: &mut Window,
cx: &mut Self::Context,
) {
let character_pos = self
.internal
.index_for_pixel_point(text_position, window.line_height());
self.internal.on_mouse_move(character_pos, cx);
}
}