remove previous input iteration
This commit is contained in:
@@ -1,25 +0,0 @@
|
||||
mod colors;
|
||||
mod cursor;
|
||||
mod element;
|
||||
mod history;
|
||||
mod layout;
|
||||
mod paint;
|
||||
mod state;
|
||||
mod state_input_handler;
|
||||
mod storage;
|
||||
pub(self) mod unicode;
|
||||
|
||||
pub use colors::*;
|
||||
pub use cursor::*;
|
||||
pub use element::*;
|
||||
pub(self) use history::*;
|
||||
pub use layout::*;
|
||||
pub use state::*;
|
||||
pub use storage::*;
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn make_element(app: &mut gpui::App) -> impl gpui::IntoElement {
|
||||
use gpui::AppContext;
|
||||
let state = app.new(|cx| InputState::new(cx));
|
||||
input(&state, app).text_cursor(default_cursor(&state, app))
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
use gpui::Hsla;
|
||||
|
||||
/// Style colors applied to the Input element
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct InputColors {
|
||||
/// This is the background color applied to the range of text that is currently selected by the user.
|
||||
pub selection: Hsla,
|
||||
/// This is the color of the placeholder string, when one is assigned and the text field is empty.
|
||||
pub placeholder: Hsla,
|
||||
pub marked: Hsla,
|
||||
}
|
||||
|
||||
impl Default for InputColors {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
selection: gpui::hsla(0.583, 0.519, 0.31, 1.0),
|
||||
marked: Hsla::white().opacity(0.6),
|
||||
placeholder: gpui::hsla(0., 0., 0.5, 1.0),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,282 +0,0 @@
|
||||
use gpui::{
|
||||
App, Bounds, Context, Element, Entity, EventEmitter, Hsla, IntoElement, Pixels, Point, Render,
|
||||
Subscription,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::input::CursorTrigger;
|
||||
|
||||
/// Default interval for cursor blinking.
|
||||
pub const DEFAULT_BLINK_INTERVAL: Duration = Duration::from_millis(500);
|
||||
|
||||
/// The state of an input's cursor blinking. While active, the cursor's visibility changes at some interval.
|
||||
/// This blinking can be temporarily paused (e.g. during typing).
|
||||
pub struct Cursor {
|
||||
state: Entity<CursorState>,
|
||||
color: Hsla,
|
||||
/// Tracks whether we were focused on the last update.
|
||||
was_focused: bool,
|
||||
point: Point<Pixels>,
|
||||
height: Pixels,
|
||||
}
|
||||
|
||||
pub struct CursorState {
|
||||
interval: Duration,
|
||||
generation: usize,
|
||||
visible: bool,
|
||||
active: bool,
|
||||
paused: bool,
|
||||
#[allow(dead_code)]
|
||||
subscriptions: SmallVec<[Subscription; 2]>,
|
||||
}
|
||||
impl Default for CursorState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
interval: Duration::ZERO,
|
||||
generation: Default::default(),
|
||||
visible: true,
|
||||
active: Default::default(),
|
||||
paused: Default::default(),
|
||||
subscriptions: SmallVec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn cursor(state: Entity<CursorState>) -> Cursor {
|
||||
Cursor::new(state)
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn default_cursor<E>(emitter: &Entity<E>, cx: &mut App) -> Cursor
|
||||
where
|
||||
E: EventEmitter<CursorTrigger>,
|
||||
{
|
||||
use gpui::AppContext;
|
||||
cursor(cx.new(|cx| {
|
||||
let mut cursor = CursorState::default().blink_interval_default();
|
||||
cursor.subscribe_to(emitter, cx);
|
||||
cursor
|
||||
}))
|
||||
}
|
||||
|
||||
impl Cursor {
|
||||
#[track_caller]
|
||||
fn new(state: Entity<CursorState>) -> Self {
|
||||
Self {
|
||||
state,
|
||||
color: Hsla::white(),
|
||||
was_focused: false,
|
||||
point: Point::default(),
|
||||
height: Pixels::ZERO,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn color(mut self, color: Hsla) -> Self {
|
||||
self.color = color;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl CursorState {
|
||||
pub fn blink_interval_default(mut self) -> Self {
|
||||
self.interval = DEFAULT_BLINK_INTERVAL;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn blink_interval(mut self, interval: Duration) -> Self {
|
||||
self.interval = interval;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn subscribe_to<E>(&mut self, emitter: &Entity<E>, cx: &mut Context<Self>)
|
||||
where
|
||||
E: EventEmitter<CursorTrigger>,
|
||||
{
|
||||
let handle = cx.subscribe(emitter, |state, _emitter, event, cx| match event {
|
||||
CursorTrigger::PauseBlinkingForUserAction => {
|
||||
if !state.interval.is_zero() {
|
||||
state.pause_blinking(cx);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
});
|
||||
self.subscriptions.push(handle);
|
||||
}
|
||||
|
||||
/// Activates cursor blinking.
|
||||
///
|
||||
/// While active, the cursor will alternate between visible and hidden states at the configured interval. Has no effect if already active.
|
||||
fn enable(&mut self, cx: &mut Context<Self>) {
|
||||
if self.active {
|
||||
return;
|
||||
}
|
||||
|
||||
self.active = true;
|
||||
self.visible = false;
|
||||
self.paused = false;
|
||||
self.spawn_ticker(cx);
|
||||
}
|
||||
|
||||
/// Deactivates cursor blinking.
|
||||
///
|
||||
/// Marks the cursor as invisible and pauses blinking indefinitely. `enable` must be called explicitly to resume visibility and blinking.
|
||||
/// Call `pause_blinking` instead if you want to temporarily stop blinking while keeping the cursor visible.
|
||||
fn disable(&mut self, cx: &mut Context<Self>) {
|
||||
self.active = false;
|
||||
self.visible = false;
|
||||
self.paused = false;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Temporarily pauses blinking and leaves the cursor visible. Blinking will resume after the pre-established interval elapses from the time this is called.
|
||||
fn pause_blinking(&mut self, cx: &mut Context<Self>) {
|
||||
if !self.visible {
|
||||
self.visible = true;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
self.paused = true;
|
||||
self.generation = self.generation.wrapping_add(1);
|
||||
|
||||
let generation = self.generation;
|
||||
let interval = self.interval;
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
async_io::Timer::after(interval).await;
|
||||
this.update(cx, |this, cx| {
|
||||
if this.generation == generation {
|
||||
this.paused = false;
|
||||
this.spawn_ticker(cx);
|
||||
}
|
||||
})
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn spawn_ticker(&mut self, cx: &mut Context<Self>) {
|
||||
if !self.active || self.paused {
|
||||
return;
|
||||
}
|
||||
|
||||
self.visible = !self.visible;
|
||||
cx.notify();
|
||||
|
||||
self.generation = self.generation.wrapping_add(1);
|
||||
let generation = self.generation;
|
||||
let interval = self.interval;
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
async_io::Timer::after(interval).await;
|
||||
if let Some(this) = this.upgrade() {
|
||||
this.update(cx, |this, cx| {
|
||||
if this.generation == generation {
|
||||
this.spawn_ticker(cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
impl Cursor {
|
||||
pub fn update_input(
|
||||
&mut self,
|
||||
is_focused: bool,
|
||||
pos: Point<Pixels>,
|
||||
line_height: Pixels,
|
||||
cx: &mut App,
|
||||
) -> bool {
|
||||
let was_focused = self.was_focused;
|
||||
self.was_focused = is_focused;
|
||||
|
||||
self.point = pos;
|
||||
self.height = line_height;
|
||||
|
||||
match (
|
||||
self.state.read(cx).interval.is_zero(),
|
||||
is_focused,
|
||||
was_focused,
|
||||
) {
|
||||
(true, _, _) => true,
|
||||
(false, true, false) => {
|
||||
self.state.update(cx, |state, cx| {
|
||||
state.enable(cx);
|
||||
});
|
||||
true
|
||||
}
|
||||
(false, false, true) => {
|
||||
self.state.update(cx, |state, cx| {
|
||||
state.disable(cx);
|
||||
});
|
||||
false
|
||||
}
|
||||
(false, _, _) => self.state.read(cx).visible,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoElement for Cursor {
|
||||
type Element = Self;
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for Cursor {
|
||||
type RequestLayoutState = ();
|
||||
type PrepaintState = ();
|
||||
|
||||
fn id(&self) -> Option<gpui::ElementId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
_id: Option<&gpui::GlobalElementId>,
|
||||
_inspector_id: Option<&gpui::InspectorElementId>,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::App,
|
||||
) -> (gpui::LayoutId, Self::RequestLayoutState) {
|
||||
let layout_id = window.request_layout(gpui::Style::default(), None, cx);
|
||||
(layout_id, ())
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
_id: Option<&gpui::GlobalElementId>,
|
||||
_inspector_id: Option<&gpui::InspectorElementId>,
|
||||
_bounds: gpui::Bounds<gpui::Pixels>,
|
||||
_request_layout: &mut Self::RequestLayoutState,
|
||||
_window: &mut gpui::Window,
|
||||
_cx: &mut gpui::App,
|
||||
) -> Self::PrepaintState {
|
||||
()
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
_id: Option<&gpui::GlobalElementId>,
|
||||
_inspector_id: Option<&gpui::InspectorElementId>,
|
||||
bounds: gpui::Bounds<gpui::Pixels>,
|
||||
_request_layout: &mut Self::RequestLayoutState,
|
||||
_prepaint: &mut Self::PrepaintState,
|
||||
window: &mut gpui::Window,
|
||||
_cx: &mut gpui::App,
|
||||
) {
|
||||
const CURSOR_WIDTH: f32 = 2.0;
|
||||
window.paint_quad(gpui::fill(
|
||||
Bounds::new(
|
||||
gpui::point(bounds.left(), bounds.top()) + self.point,
|
||||
gpui::size(gpui::px(CURSOR_WIDTH), self.height),
|
||||
),
|
||||
self.color,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -1,205 +0,0 @@
|
||||
use crate::input::{Cursor, InputColors, InputState};
|
||||
use gpui::{
|
||||
Action, AnyElement, App, Context, Entity, FocusHandle, Focusable, Hsla, InteractiveElement,
|
||||
Interactivity, IntoElement, SharedString, StyleRefinement, Styled, Window,
|
||||
};
|
||||
|
||||
#[track_caller]
|
||||
pub fn input(input_state: &Entity<InputState>, cx: &App) -> Input {
|
||||
Input::new(input_state, cx)
|
||||
}
|
||||
|
||||
/// A text editing element that supports both single-line and multi-line modes.
|
||||
pub struct Input {
|
||||
pub(super) input: Entity<InputState>,
|
||||
pub(super) interactivity: Interactivity,
|
||||
pub(super) placeholder: Option<SharedString>,
|
||||
pub(super) colors: InputColors,
|
||||
pub(super) cursor: Option<Cursor>,
|
||||
}
|
||||
|
||||
impl Input {
|
||||
#[track_caller]
|
||||
fn new(input_state: &Entity<InputState>, cx: &App) -> Self {
|
||||
let focus_handle = input_state.focus_handle(cx);
|
||||
let mut input = Input {
|
||||
input: input_state.clone(),
|
||||
interactivity: Interactivity::new(),
|
||||
placeholder: None,
|
||||
colors: InputColors::default(),
|
||||
cursor: None,
|
||||
};
|
||||
input.register_actions();
|
||||
input
|
||||
.key_context(crate::editable_text::actions::DEFAULT_INPUT_CONTEXT)
|
||||
.track_focus(&focus_handle)
|
||||
}
|
||||
|
||||
fn register_actions(&mut self) {
|
||||
register_action(&mut self.interactivity, &self.input, InputState::left);
|
||||
register_action(&mut self.interactivity, &self.input, InputState::right);
|
||||
register_action(&mut self.interactivity, &self.input, InputState::up);
|
||||
register_action(&mut self.interactivity, &self.input, InputState::down);
|
||||
register_action(
|
||||
&mut self.interactivity,
|
||||
&self.input,
|
||||
InputState::select_left,
|
||||
);
|
||||
register_action(
|
||||
&mut self.interactivity,
|
||||
&self.input,
|
||||
InputState::select_right,
|
||||
);
|
||||
register_action(&mut self.interactivity, &self.input, InputState::select_up);
|
||||
register_action(
|
||||
&mut self.interactivity,
|
||||
&self.input,
|
||||
InputState::select_down,
|
||||
);
|
||||
register_action(&mut self.interactivity, &self.input, InputState::select_all);
|
||||
register_action(&mut self.interactivity, &self.input, InputState::home);
|
||||
register_action(&mut self.interactivity, &self.input, InputState::end);
|
||||
register_action(
|
||||
&mut self.interactivity,
|
||||
&self.input,
|
||||
InputState::move_to_beginning,
|
||||
);
|
||||
register_action(
|
||||
&mut self.interactivity,
|
||||
&self.input,
|
||||
InputState::move_to_end,
|
||||
);
|
||||
register_action(
|
||||
&mut self.interactivity,
|
||||
&self.input,
|
||||
InputState::select_to_beginning,
|
||||
);
|
||||
register_action(
|
||||
&mut self.interactivity,
|
||||
&self.input,
|
||||
InputState::select_to_end,
|
||||
);
|
||||
register_action(&mut self.interactivity, &self.input, InputState::word_left);
|
||||
register_action(&mut self.interactivity, &self.input, InputState::word_right);
|
||||
register_action(
|
||||
&mut self.interactivity,
|
||||
&self.input,
|
||||
InputState::select_word_left,
|
||||
);
|
||||
register_action(
|
||||
&mut self.interactivity,
|
||||
&self.input,
|
||||
InputState::select_word_right,
|
||||
);
|
||||
register_action(&mut self.interactivity, &self.input, InputState::backspace);
|
||||
register_action(&mut self.interactivity, &self.input, InputState::delete);
|
||||
register_action(
|
||||
&mut self.interactivity,
|
||||
&self.input,
|
||||
InputState::delete_word_left,
|
||||
);
|
||||
register_action(
|
||||
&mut self.interactivity,
|
||||
&self.input,
|
||||
InputState::delete_word_right,
|
||||
);
|
||||
register_action(
|
||||
&mut self.interactivity,
|
||||
&self.input,
|
||||
InputState::delete_to_beginning_of_line,
|
||||
);
|
||||
register_action(
|
||||
&mut self.interactivity,
|
||||
&self.input,
|
||||
InputState::delete_to_end_of_line,
|
||||
);
|
||||
register_action(&mut self.interactivity, &self.input, InputState::enter);
|
||||
register_action(&mut self.interactivity, &self.input, InputState::tab);
|
||||
register_action(&mut self.interactivity, &self.input, InputState::paste);
|
||||
register_action(&mut self.interactivity, &self.input, InputState::copy);
|
||||
register_action(&mut self.interactivity, &self.input, InputState::cut);
|
||||
register_action(&mut self.interactivity, &self.input, InputState::undo);
|
||||
register_action(&mut self.interactivity, &self.input, InputState::redo);
|
||||
|
||||
self.interactivity
|
||||
.on_action::<crate::editable_text::actions::Escape>(|_action, window, _cx| {
|
||||
window.blur();
|
||||
});
|
||||
}
|
||||
|
||||
pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
|
||||
self.placeholder = Some(placeholder.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the styling colors for the input element
|
||||
pub fn colors(mut self, colors: InputColors) -> Self {
|
||||
self.colors = colors;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the "selection" color for the input element.
|
||||
/// This is the background color applied to the range of text that is currently selected by the user.
|
||||
pub fn selection_color(mut self, color: Hsla) -> Self {
|
||||
self.colors.selection = color;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the "placeholder" color for the input element.
|
||||
/// This is the color of the placeholder string, when one is assigned and the text field is empty.
|
||||
pub fn placeholder_color(mut self, color: Hsla) -> Self {
|
||||
self.colors.placeholder = color;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the "marked" color for the input element.
|
||||
/// Marking text comes from IME and needs further doc clarification.
|
||||
pub fn marked_color(mut self, color: Hsla) -> Self {
|
||||
self.colors.marked = color;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn text_cursor(mut self, cursor: Cursor) -> Self {
|
||||
self.cursor = Some(cursor);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
fn register_action<A: Action>(
|
||||
interactivity: &mut Interactivity,
|
||||
input: &Entity<InputState>,
|
||||
listener: fn(&mut InputState, &A, &mut Window, &mut Context<InputState>),
|
||||
) {
|
||||
let input = input.clone();
|
||||
interactivity.on_action::<A>(move |action, window, cx| {
|
||||
input.update(cx, |input, cx| {
|
||||
listener(input, action, window, cx);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
impl Styled for Input {
|
||||
fn style(&mut self) -> &mut StyleRefinement {
|
||||
&mut self.interactivity.base_style
|
||||
}
|
||||
}
|
||||
|
||||
impl InteractiveElement for Input {
|
||||
fn interactivity(&mut self) -> &mut Interactivity {
|
||||
&mut self.interactivity
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for Input {
|
||||
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||
self.input.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoElement for Input {
|
||||
type Element = Self;
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
use gpui::NavigationDirection;
|
||||
use std::{
|
||||
ops::Range,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use crate::input::InputStorage;
|
||||
|
||||
/// Maximum number of history entries to keep.
|
||||
pub const MAX_HISTORY_LEN: usize = 1000;
|
||||
|
||||
/// Default interval for grouping consecutive edits into a single undo entry.
|
||||
pub const DEFAULT_GROUP_INTERVAL: Duration = Duration::from_millis(300);
|
||||
|
||||
/// A patch-based history entry for memory-efficient undo/redo operations.
|
||||
/// Instead of storing the full content, we store only the change needed to reverse the edit.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct HistoryEntry {
|
||||
/// The byte range that was modified (after the edit, for undo; before the edit, for redo).
|
||||
pub range: Range<usize>,
|
||||
/// The text that was replaced (to restore on undo).
|
||||
pub old_text: String,
|
||||
/// The length of the new text that replaced old_text (to know how much to remove on undo).
|
||||
pub new_text_len: usize,
|
||||
/// The selection range before the edit.
|
||||
pub selected_range: Range<usize>,
|
||||
/// The direction of the selection before the edit.
|
||||
pub selection_direction: NavigationDirection,
|
||||
/// Timestamp for grouping consecutive edits.
|
||||
pub timestamp: Instant,
|
||||
}
|
||||
|
||||
impl HistoryEntry {
|
||||
/// Apply this patch to undo an edit, returning the reverse patch for redo.
|
||||
pub fn apply_undo(&self, content: &mut Box<dyn InputStorage>) -> HistoryEntry {
|
||||
let undo_start = self.range.start;
|
||||
let undo_end = (self.range.start + self.new_text_len).min(content.len());
|
||||
|
||||
// Capture what we're about to remove (the "new" text that was inserted)
|
||||
let removed_text = content.as_str()[undo_start..undo_end].to_string();
|
||||
|
||||
// Replace with the old text
|
||||
content.replace_range(undo_start..undo_end, &self.old_text);
|
||||
|
||||
// Return reverse patch for redo
|
||||
HistoryEntry {
|
||||
range: undo_start..undo_start + self.old_text.len(),
|
||||
old_text: removed_text,
|
||||
new_text_len: self.old_text.len(),
|
||||
selected_range: self.selected_range.clone(),
|
||||
selection_direction: self.selection_direction,
|
||||
timestamp: self.timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply this patch to redo an edit, returning the reverse patch for undo.
|
||||
pub fn apply_redo(&self, content: &mut Box<dyn InputStorage>) -> HistoryEntry {
|
||||
// Redo is the same operation as undo - we're reversing the undo
|
||||
self.apply_undo(content)
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum InputLayoutStyle {
|
||||
SingleLine,
|
||||
MultiLine,
|
||||
}
|
||||
|
||||
impl InputLayoutStyle {
|
||||
pub(super) fn sanitize_content<'s>(&self, content: &'s str) -> std::borrow::Cow<'s, str> {
|
||||
match self {
|
||||
// Strip newlines for single-line input
|
||||
Self::SingleLine => {
|
||||
std::borrow::Cow::Owned(content.replace('\n', " ").replace('\r', ""))
|
||||
}
|
||||
Self::MultiLine => std::borrow::Cow::Borrowed(content),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn axis(&self) -> gpui::Axis {
|
||||
match self {
|
||||
Self::SingleLine => gpui::Axis::Horizontal,
|
||||
Self::MultiLine => gpui::Axis::Vertical,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,685 +0,0 @@
|
||||
use crate::input::{Cursor, Input, InputColors, InputLayoutData, InputLogicalLine, InputState};
|
||||
use gpui::{
|
||||
Along, App, Axis, Bounds, ContentMask, CursorStyle, DispatchPhase, Display, Element, ElementId,
|
||||
ElementInputHandler, Entity, Focusable, GlobalElementId, Hitbox, HitboxBehavior, Hsla,
|
||||
InspectorElementId, LayoutId, Length, MouseButton, MouseDownEvent, MouseMoveEvent,
|
||||
MouseUpEvent, Pixels, Point, ScrollWheelEvent, SharedString, Style, TextAlign, TextRun,
|
||||
TextStyle, Window, fill, point, px, relative, size,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use std::ops::Range;
|
||||
|
||||
const MARKED_TEXT_UNDERLINE_THICKNESS: f32 = 2.0;
|
||||
|
||||
pub struct InputLayoutState {
|
||||
text_style: TextStyle,
|
||||
#[allow(dead_code)]
|
||||
child_layout_ids: SmallVec<[LayoutId; 2]>,
|
||||
cursor_layout: Option<<Cursor as Element>::RequestLayoutState>,
|
||||
}
|
||||
|
||||
pub struct InputPrepaintState {
|
||||
hitbox: Option<Hitbox>,
|
||||
cursor_prepaint: Option<<Cursor as Element>::PrepaintState>,
|
||||
}
|
||||
|
||||
impl Element for Input {
|
||||
type RequestLayoutState = InputLayoutState;
|
||||
type PrepaintState = InputPrepaintState;
|
||||
|
||||
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<&GlobalElementId>,
|
||||
inspector_id: Option<&InspectorElementId>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
let mut resolved_text_style = None;
|
||||
let mut child_layout_ids = SmallVec::new();
|
||||
let mut cursor_layout = None;
|
||||
|
||||
let layout_id = self.interactivity.request_layout(
|
||||
global_id,
|
||||
inspector_id,
|
||||
window,
|
||||
cx,
|
||||
|element_style, window, cx| {
|
||||
window.with_text_style(element_style.text_style().cloned(), |window| {
|
||||
let state = self.input.read(cx);
|
||||
|
||||
resolved_text_style = Some(window.text_style());
|
||||
|
||||
let mut layout_style = element_style.clone();
|
||||
if matches!(state.layout_style(), super::InputLayoutStyle::MultiLine) {
|
||||
if let Length::Auto = layout_style.size.width {
|
||||
layout_style.size.width = relative(1.).into();
|
||||
}
|
||||
if let Length::Auto = layout_style.size.height {
|
||||
layout_style.size.height = relative(1.).into();
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(cursor) = &mut self.cursor {
|
||||
let (layout_id, layout) =
|
||||
cursor.request_layout(global_id, inspector_id, window, cx);
|
||||
child_layout_ids.push(layout_id);
|
||||
cursor_layout = Some(layout);
|
||||
}
|
||||
|
||||
window.request_layout(layout_style, child_layout_ids.iter().copied(), cx)
|
||||
})
|
||||
},
|
||||
);
|
||||
|
||||
let layout_state = InputLayoutState {
|
||||
text_style: resolved_text_style.unwrap_or_else(|| window.text_style()),
|
||||
child_layout_ids,
|
||||
cursor_layout,
|
||||
};
|
||||
(layout_id, layout_state)
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
inspector_id: Option<&InspectorElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
layout_state: &mut Self::RequestLayoutState,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Self::PrepaintState {
|
||||
let line_height = layout_state
|
||||
.text_style
|
||||
.line_height_in_pixels(window.rem_size());
|
||||
|
||||
let wrap_width = match self.input.read(cx).layout_style() {
|
||||
super::InputLayoutStyle::SingleLine => None,
|
||||
super::InputLayoutStyle::MultiLine => Some(bounds.size.width),
|
||||
};
|
||||
|
||||
self.input.update(cx, |input, _cx| {
|
||||
let layout_data = InputLayoutData {
|
||||
text_style: layout_state.text_style.clone(),
|
||||
line_height,
|
||||
wrap_width,
|
||||
available_size: bounds.size,
|
||||
dirty: false,
|
||||
};
|
||||
input.apply_layout_update(layout_data, window);
|
||||
});
|
||||
|
||||
let mut cursor_prepaint = None;
|
||||
let hitbox = self.interactivity.prepaint(
|
||||
global_id,
|
||||
inspector_id,
|
||||
bounds,
|
||||
bounds.size,
|
||||
window,
|
||||
cx,
|
||||
|style, scroll_offset, hitbox, window, cx| {
|
||||
let hitbox =
|
||||
hitbox.or_else(|| Some(window.insert_hitbox(bounds, HitboxBehavior::Normal)));
|
||||
|
||||
if style.display != Display::None {
|
||||
window.with_element_offset(scroll_offset, |window| {
|
||||
match (&mut self.cursor, &mut layout_state.cursor_layout) {
|
||||
(Some(cursor), Some(layout)) => {
|
||||
let prepaint = cursor.prepaint(
|
||||
global_id,
|
||||
inspector_id,
|
||||
bounds,
|
||||
layout,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
cursor_prepaint = Some(prepaint);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
hitbox
|
||||
},
|
||||
);
|
||||
|
||||
InputPrepaintState {
|
||||
hitbox,
|
||||
cursor_prepaint,
|
||||
}
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
inspector_id: Option<&InspectorElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
layout_state: &mut Self::RequestLayoutState,
|
||||
prepaint_state: &mut Self::PrepaintState,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let focus_handle = self.input.focus_handle(cx);
|
||||
|
||||
if let Some(hitbox) = &prepaint_state.hitbox {
|
||||
window.set_cursor_style(CursorStyle::IBeam, hitbox);
|
||||
}
|
||||
|
||||
window.handle_input(
|
||||
&focus_handle,
|
||||
ElementInputHandler::new(bounds, self.input.clone()),
|
||||
cx,
|
||||
);
|
||||
|
||||
let snapshot = InputStateSnapshot::new(&self.input, cx);
|
||||
let placeholder = self.placeholder.clone();
|
||||
let text_style = layout_state.text_style.clone();
|
||||
let is_focused = focus_handle.is_focused(window);
|
||||
let colors = self.colors;
|
||||
|
||||
let perform_paint = |style: &Style, window: &mut Window, cx: &mut App| {
|
||||
if style.display == Display::None {
|
||||
return;
|
||||
}
|
||||
|
||||
let context = PaintContext {
|
||||
snapshot,
|
||||
is_focused,
|
||||
bounds,
|
||||
text_style: &text_style,
|
||||
placeholder: placeholder.as_ref(),
|
||||
colors: &colors,
|
||||
};
|
||||
context.process_mouse_events(&self.input, window, cx);
|
||||
window.with_content_mask(Some(ContentMask { bounds }), |window| {
|
||||
context.paint(window, cx);
|
||||
|
||||
match (
|
||||
&mut self.cursor,
|
||||
&mut layout_state.cursor_layout,
|
||||
&mut prepaint_state.cursor_prepaint,
|
||||
) {
|
||||
(Some(cursor), Some(layout), Some(prepaint)) => {
|
||||
let cursor_pos = context.find_cursor_position_in_layouts();
|
||||
let visible = cursor.update_input(
|
||||
is_focused,
|
||||
cursor_pos,
|
||||
context.snapshot.line_height,
|
||||
cx,
|
||||
);
|
||||
if is_focused && visible && context.snapshot.selected_range.is_empty() {
|
||||
cursor.paint(
|
||||
global_id,
|
||||
inspector_id,
|
||||
bounds,
|
||||
layout,
|
||||
prepaint,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
};
|
||||
self.interactivity.paint(
|
||||
global_id,
|
||||
inspector_id,
|
||||
bounds,
|
||||
prepaint_state.hitbox.as_ref(),
|
||||
window,
|
||||
cx,
|
||||
perform_paint,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A minimal copy of InputState that is used during paint operations without needing to read from the entity in App multiple times in a single paint.
|
||||
/// Ideally this struct is quite small.
|
||||
struct InputStateSnapshot {
|
||||
layout_axis: Axis,
|
||||
should_center_placeholder: bool,
|
||||
show_placeholder: bool,
|
||||
selected_range: Range<usize>,
|
||||
marked_range: Option<Range<usize>>,
|
||||
cursor_position: usize,
|
||||
logical_lines: Vec<InputLogicalLine>,
|
||||
scroll_distance: Pixels,
|
||||
line_height: Pixels,
|
||||
}
|
||||
impl InputStateSnapshot {
|
||||
fn new(entity: &Entity<InputState>, cx: &App) -> Self {
|
||||
let input_state = entity.read(cx);
|
||||
let selected_range = input_state.selected_range().clone();
|
||||
let marked_range = input_state.marked_range().cloned();
|
||||
let cursor_position = input_state.cursor_position();
|
||||
let logical_lines = input_state.lines().clone();
|
||||
let scroll_distance = input_state.distance_from_top();
|
||||
let line_height = input_state.line_height();
|
||||
let layout_axis = input_state.layout_style().axis();
|
||||
let should_center_placeholder = matches!(
|
||||
input_state.layout_style(),
|
||||
super::InputLayoutStyle::SingleLine
|
||||
);
|
||||
Self {
|
||||
layout_axis,
|
||||
should_center_placeholder,
|
||||
show_placeholder: input_state.content().as_str().is_empty(),
|
||||
selected_range,
|
||||
marked_range,
|
||||
cursor_position,
|
||||
logical_lines,
|
||||
scroll_distance,
|
||||
line_height,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct PaintContext<'app> {
|
||||
snapshot: InputStateSnapshot,
|
||||
is_focused: bool,
|
||||
bounds: Bounds<Pixels>,
|
||||
text_style: &'app TextStyle,
|
||||
placeholder: Option<&'app SharedString>,
|
||||
colors: &'app InputColors,
|
||||
}
|
||||
|
||||
impl<'app> PaintContext<'app> {
|
||||
pub fn process_mouse_events(
|
||||
&self,
|
||||
entity: &Entity<InputState>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let axis = self.snapshot.layout_axis;
|
||||
let bounds = self.bounds;
|
||||
let scroll_distance = self.snapshot.scroll_distance;
|
||||
window.on_mouse_event({
|
||||
let input = entity.clone();
|
||||
move |event: &MouseDownEvent, phase, window, cx| {
|
||||
if phase != DispatchPhase::Bubble {
|
||||
return;
|
||||
}
|
||||
if !bounds.contains(&event.position) {
|
||||
return;
|
||||
}
|
||||
if event.button != MouseButton::Left {
|
||||
return;
|
||||
}
|
||||
|
||||
input.update(cx, |input, cx| {
|
||||
// Converts a screen position to a position relative to the text area origin, adjusted for scroll offset.
|
||||
let text_position = (event.position - bounds.origin)
|
||||
.apply_along(axis, |pos| pos + scroll_distance);
|
||||
input.on_mouse_down(
|
||||
text_position,
|
||||
event.click_count,
|
||||
event.modifiers.shift,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
window.on_mouse_event({
|
||||
let input = entity.clone();
|
||||
move |event: &MouseUpEvent, phase, _window, cx| {
|
||||
if phase != DispatchPhase::Bubble {
|
||||
return;
|
||||
}
|
||||
if event.button != MouseButton::Left {
|
||||
return;
|
||||
}
|
||||
|
||||
input.update(cx, |input, cx| {
|
||||
input.on_mouse_up(cx);
|
||||
});
|
||||
}
|
||||
});
|
||||
window.on_mouse_event({
|
||||
let input = entity.clone();
|
||||
move |event: &MouseMoveEvent, phase, _window, cx| {
|
||||
if phase != DispatchPhase::Bubble {
|
||||
return;
|
||||
}
|
||||
|
||||
input.update(cx, |input, cx| {
|
||||
// Converts a screen position to a position relative to the text area origin, adjusted for scroll offset.
|
||||
let text_position = (event.position - bounds.origin)
|
||||
.apply_along(axis, |pos| pos + scroll_distance);
|
||||
input.on_mouse_move(text_position, cx);
|
||||
});
|
||||
}
|
||||
});
|
||||
window.on_mouse_event({
|
||||
let input = entity.clone();
|
||||
let content_size = match axis {
|
||||
gpui::Axis::Horizontal => {
|
||||
let state = input.read(cx);
|
||||
let line = state.lines().first();
|
||||
let line = line.and_then(|l| l.wrapped_line.as_ref());
|
||||
line.map(|w| w.width()).unwrap_or(px(0.))
|
||||
}
|
||||
gpui::Axis::Vertical => input.read(cx).total_content_height(),
|
||||
};
|
||||
let max_scroll = (content_size - bounds.size.along(axis)).max(px(0.));
|
||||
move |event: &ScrollWheelEvent, phase, _window, cx| {
|
||||
if phase != DispatchPhase::Bubble {
|
||||
return;
|
||||
}
|
||||
if !bounds.contains(&event.position) {
|
||||
return;
|
||||
}
|
||||
|
||||
let pixel_delta = event.delta.pixel_delta(px(20.));
|
||||
input.update(cx, |input, cx| {
|
||||
let delta = match axis {
|
||||
gpui::Axis::Horizontal => pixel_delta.y,
|
||||
gpui::Axis::Vertical => {
|
||||
if pixel_delta.x.abs() > pixel_delta.y.abs() {
|
||||
pixel_delta.x
|
||||
} else {
|
||||
pixel_delta.y
|
||||
}
|
||||
}
|
||||
};
|
||||
input.apply_scroll_delta(delta, max_scroll);
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn paint_bounds_quad(
|
||||
&self,
|
||||
window: &mut Window,
|
||||
color: Hsla,
|
||||
offset_start: Point<Pixels>,
|
||||
offset_end: Point<Pixels>,
|
||||
) {
|
||||
let top_left = point(self.bounds.left(), self.bounds.top());
|
||||
window.paint_quad(fill(
|
||||
Bounds::from_corners(top_left + offset_start, top_left + offset_end),
|
||||
color,
|
||||
));
|
||||
}
|
||||
|
||||
pub fn paint(&self, window: &mut Window, cx: &mut App) {
|
||||
if !self.snapshot.selected_range.is_empty() {
|
||||
self.paint_selection(window);
|
||||
}
|
||||
|
||||
if self.snapshot.show_placeholder {
|
||||
self.paint_placeholder(window, cx);
|
||||
} else {
|
||||
self.paint_text(window, cx);
|
||||
}
|
||||
|
||||
self.paint_marked_underline(window);
|
||||
}
|
||||
|
||||
fn paint_selection(&self, window: &mut Window) {
|
||||
let one_line = self.snapshot.logical_lines.len() == 1;
|
||||
for line in &self.snapshot.logical_lines {
|
||||
let line_y = line.y_offset - self.snapshot.scroll_distance;
|
||||
|
||||
if !one_line {
|
||||
if !self.is_line_visible(line) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if !line_intersects_range(&line.text_range, &self.snapshot.selected_range) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if line.text_range.is_empty() {
|
||||
const EMPTY_LINE_SELECTION_WIDTH: Pixels = px(6.);
|
||||
self.paint_bounds_quad(
|
||||
window,
|
||||
self.colors.selection,
|
||||
point(px(0.), line_y),
|
||||
point(
|
||||
EMPTY_LINE_SELECTION_WIDTH,
|
||||
line_y + self.snapshot.line_height,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
self.paint_line_range(
|
||||
window,
|
||||
line,
|
||||
&self.snapshot.selected_range,
|
||||
self.colors.selection,
|
||||
px(0.),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_placeholder(&self, window: &mut Window, cx: &mut App) {
|
||||
let Some(placeholder) = self.placeholder else {
|
||||
return;
|
||||
};
|
||||
if placeholder.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let run = TextRun {
|
||||
len: placeholder.len(),
|
||||
font: self.text_style.font(),
|
||||
color: self.colors.placeholder,
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
};
|
||||
|
||||
let font_size = self.text_style.font_size.to_pixels(window.rem_size());
|
||||
let shaped_line =
|
||||
window
|
||||
.text_system()
|
||||
.shape_line(placeholder.clone(), font_size, &[run], None);
|
||||
let line_height = self.text_style.line_height_in_pixels(window.rem_size());
|
||||
|
||||
let mut paint_origin = self.bounds.origin;
|
||||
if self.snapshot.should_center_placeholder {
|
||||
let y_offset = (self.bounds.size.height - line_height).max(px(0.)) / 2.0;
|
||||
paint_origin.y += y_offset;
|
||||
}
|
||||
|
||||
let _ = shaped_line.paint(paint_origin, line_height, TextAlign::Left, None, window, cx);
|
||||
}
|
||||
|
||||
fn paint_text(&self, window: &mut Window, cx: &mut App) {
|
||||
for line_layout in &self.snapshot.logical_lines {
|
||||
let line_y = line_layout.y_offset - self.snapshot.scroll_distance;
|
||||
|
||||
if !self.is_line_visible(line_layout) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(wrapped) = &line_layout.wrapped_line else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let paint_pos = point(self.bounds.left(), self.bounds.top() + line_y);
|
||||
let _ = wrapped.paint(
|
||||
paint_pos,
|
||||
self.snapshot.line_height,
|
||||
TextAlign::Left,
|
||||
Some(self.bounds),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_marked_underline(&self, window: &mut Window) {
|
||||
let Some(marked_range) = &self.snapshot.marked_range else {
|
||||
return;
|
||||
};
|
||||
if marked_range.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let underline_thickness = px(MARKED_TEXT_UNDERLINE_THICKNESS);
|
||||
let underline_offset = self.snapshot.line_height - underline_thickness;
|
||||
for line in &self.snapshot.logical_lines {
|
||||
if !self.is_line_visible(line) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if !line_intersects_range(&line.text_range, marked_range) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if line.text_range.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
self.paint_line_range(
|
||||
window,
|
||||
line,
|
||||
marked_range,
|
||||
self.colors.marked,
|
||||
underline_offset,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn find_cursor_position_in_layouts(&self) -> Point<Pixels> {
|
||||
for line in &self.snapshot.logical_lines {
|
||||
let line_y = line.y_offset - self.snapshot.scroll_distance;
|
||||
|
||||
if !self.is_line_visible(line) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Since range is non-inclusive of the end value we need to check for it explicitly
|
||||
let is_cursor_in_line = if line.text_range.is_empty() {
|
||||
self.snapshot.cursor_position == line.text_range.start
|
||||
} else {
|
||||
line.text_range.contains(&self.snapshot.cursor_position)
|
||||
|| self.snapshot.cursor_position == line.text_range.end
|
||||
};
|
||||
|
||||
if !is_cursor_in_line {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(wrapped) = &line.wrapped_line else {
|
||||
return Point::default();
|
||||
};
|
||||
let local_offset = self
|
||||
.snapshot
|
||||
.cursor_position
|
||||
.saturating_sub(line.text_range.start);
|
||||
let cursor_pos = wrapped
|
||||
.position_for_index(local_offset, self.snapshot.line_height)
|
||||
.unwrap_or_default();
|
||||
return cursor_pos + point(px(0.), line_y);
|
||||
}
|
||||
Point::default()
|
||||
}
|
||||
|
||||
fn is_line_visible(&self, line: &InputLogicalLine) -> bool {
|
||||
let line_y = line.y_offset - self.snapshot.scroll_distance;
|
||||
let line_bottom = line_y + self.snapshot.line_height * line.visual_line_count as f32;
|
||||
line_bottom >= px(0.) && line_y <= self.bounds.size.height
|
||||
}
|
||||
|
||||
fn compute_visual_line_index(&self, y: Pixels) -> usize {
|
||||
(y / self.snapshot.line_height).floor() as usize
|
||||
}
|
||||
|
||||
fn paint_line_range(
|
||||
&self,
|
||||
window: &mut Window,
|
||||
line: &InputLogicalLine,
|
||||
subrange: &Range<usize>,
|
||||
color: Hsla,
|
||||
quad_offset_y: Pixels,
|
||||
) {
|
||||
let Some(wrapped) = &line.wrapped_line else {
|
||||
return;
|
||||
};
|
||||
|
||||
let line_y = line.y_offset - self.snapshot.scroll_distance;
|
||||
|
||||
let line_start = line.text_range.start;
|
||||
let line_end = line.text_range.end;
|
||||
|
||||
let subrange_start = subrange.start.max(line_start) - line_start;
|
||||
let subrange_end = subrange.end.min(line_end) - line_start;
|
||||
|
||||
let start_pos = wrapped
|
||||
.position_for_index(subrange_start, self.snapshot.line_height)
|
||||
.unwrap_or_default();
|
||||
let end_pos = wrapped
|
||||
.position_for_index(subrange_end, self.snapshot.line_height)
|
||||
.unwrap_or_else(|| {
|
||||
let last_line_y = self.snapshot.line_height * (line.visual_line_count - 1) as f32;
|
||||
point(wrapped.width(), last_line_y)
|
||||
});
|
||||
|
||||
let start_visual_line = self.compute_visual_line_index(start_pos.y);
|
||||
let end_visual_line = self.compute_visual_line_index(end_pos.y);
|
||||
|
||||
if start_visual_line == end_visual_line {
|
||||
self.paint_bounds_quad(
|
||||
window,
|
||||
color,
|
||||
point(start_pos.x, line_y + start_pos.y + quad_offset_y),
|
||||
point(end_pos.x, line_y + start_pos.y + self.snapshot.line_height),
|
||||
);
|
||||
} else {
|
||||
let line_width = wrapped.width();
|
||||
|
||||
// First visual line
|
||||
self.paint_bounds_quad(
|
||||
window,
|
||||
color,
|
||||
point(start_pos.x, line_y + start_pos.y + quad_offset_y),
|
||||
point(line_width, line_y + start_pos.y + self.snapshot.line_height),
|
||||
);
|
||||
|
||||
// Middle visual lines
|
||||
for visual_line in (start_visual_line + 1)..end_visual_line {
|
||||
let y = self.snapshot.line_height * visual_line as f32;
|
||||
self.paint_bounds_quad(
|
||||
window,
|
||||
color,
|
||||
point(px(0.), line_y + y + quad_offset_y),
|
||||
point(line_width, line_y + y + self.snapshot.line_height),
|
||||
);
|
||||
}
|
||||
|
||||
// Last visual line
|
||||
self.paint_bounds_quad(
|
||||
window,
|
||||
color,
|
||||
point(px(0.), line_y + end_pos.y + quad_offset_y),
|
||||
point(end_pos.x, line_y + end_pos.y + self.snapshot.line_height),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn line_intersects_range(
|
||||
text_range: &std::ops::Range<usize>,
|
||||
selected_range: &std::ops::Range<usize>,
|
||||
) -> bool {
|
||||
if text_range.is_empty() {
|
||||
selected_range.start <= text_range.start && selected_range.end > text_range.start
|
||||
} else {
|
||||
selected_range.end > text_range.start && selected_range.start < text_range.end
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,197 +0,0 @@
|
||||
use crate::input::{CursorTrigger, InputStateEvent};
|
||||
use gpui::{
|
||||
Bounds, Context, EntityInputHandler, NavigationDirection, Pixels, Point, UTF16Selection,
|
||||
Window, point, px,
|
||||
};
|
||||
use std::ops::Range;
|
||||
|
||||
impl EntityInputHandler for super::InputState {
|
||||
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.content().utf_range_16to8(&range_utf16);
|
||||
let clamped_range =
|
||||
range.start.min(self.content().len())..range.end.min(self.content().len());
|
||||
adjusted_range.replace(self.content().utf_range_8to16(&clamped_range));
|
||||
Some(self.content().as_str()[clamped_range].to_string())
|
||||
}
|
||||
|
||||
fn selected_text_range(
|
||||
&mut self,
|
||||
_ignore_disabled_input: bool,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Self>,
|
||||
) -> Option<UTF16Selection> {
|
||||
Some(UTF16Selection {
|
||||
range: self.content().utf_range_8to16(self.selected_range()),
|
||||
reversed: self.selection_direction() == 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.content().utf_range_8to16(range))
|
||||
}
|
||||
|
||||
fn unmark_text(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {
|
||||
self.set_marked_range(None);
|
||||
}
|
||||
|
||||
fn replace_text_in_range(
|
||||
&mut self,
|
||||
range_utf16: Option<Range<usize>>,
|
||||
new_text: &str,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let range = range_utf16
|
||||
.as_ref()
|
||||
.map(|range_utf16| self.content().utf_range_16to8(range_utf16))
|
||||
.or(self.marked_range().cloned())
|
||||
.unwrap_or(self.selected_range().clone());
|
||||
let range = range.start.min(self.content().len())..range.end.min(self.content().len());
|
||||
|
||||
let text_to_insert = self.layout_style().sanitize_content(new_text);
|
||||
|
||||
// Record patch for undo before modifying content
|
||||
self.push_undo_patch(range.clone(), text_to_insert.len());
|
||||
|
||||
self.update_utf16_len(range.clone(), &text_to_insert);
|
||||
self.replace_text_at_range(range.clone(), &text_to_insert);
|
||||
self.set_selected_range(
|
||||
range.start + text_to_insert.len()..range.start + text_to_insert.len(),
|
||||
);
|
||||
self.set_marked_range(None);
|
||||
self.mark_layout_dirty();
|
||||
|
||||
cx.emit(CursorTrigger::PauseBlinkingForUserAction);
|
||||
cx.emit(InputStateEvent::TextChanged);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn replace_and_mark_text_in_range(
|
||||
&mut self,
|
||||
range_utf16: Option<Range<usize>>,
|
||||
new_text: &str,
|
||||
new_selected_range_utf16: Option<Range<usize>>,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let range = range_utf16
|
||||
.as_ref()
|
||||
.map(|range_utf16| self.content().utf_range_16to8(range_utf16))
|
||||
.or(self.marked_range().cloned())
|
||||
.unwrap_or(self.selected_range().clone());
|
||||
let range = range.start.min(self.content().len())..range.end.min(self.content().len());
|
||||
|
||||
let text_to_insert = self.layout_style().sanitize_content(new_text);
|
||||
|
||||
self.update_utf16_len(range.clone(), &text_to_insert);
|
||||
self.replace_text_at_range(range.clone(), &text_to_insert);
|
||||
self.set_marked_range(match text_to_insert.is_empty() {
|
||||
true => None,
|
||||
false => Some(range.start..range.start + text_to_insert.len()),
|
||||
});
|
||||
self.set_selected_range({
|
||||
let new_range = new_selected_range_utf16.as_ref();
|
||||
let new_range =
|
||||
new_range.map(|range_utf16| self.content().utf_range_16to8(range_utf16));
|
||||
let new_range = new_range
|
||||
.map(|new_range| new_range.start + range.start..new_range.end + range.start);
|
||||
new_range.unwrap_or_else(|| {
|
||||
range.start + text_to_insert.len()..range.start + text_to_insert.len()
|
||||
})
|
||||
});
|
||||
self.mark_layout_dirty();
|
||||
|
||||
cx.emit(InputStateEvent::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.content().utf_range_16to8(&range_utf16);
|
||||
|
||||
for line in self.lines() {
|
||||
if line.text_range.is_empty() {
|
||||
if range.start == line.text_range.start {
|
||||
return Some(Bounds::from_corners(
|
||||
point(bounds.left(), bounds.top() + line.y_offset),
|
||||
point(
|
||||
bounds.left() + px(4.),
|
||||
bounds.top() + line.y_offset + self.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, self.line_height())
|
||||
.unwrap_or(point(px(0.), px(0.)));
|
||||
let end_pos = wrapped
|
||||
.position_for_index(local_end, self.line_height())
|
||||
.unwrap_or_else(|| {
|
||||
let last_line_y =
|
||||
self.line_height() * (line.visual_line_count - 1) as f32;
|
||||
point(wrapped.width(), last_line_y)
|
||||
});
|
||||
|
||||
let start_visual_line = (start_pos.y / self.line_height()).floor() as usize;
|
||||
let end_visual_line = (end_pos.y / self.line_height()).floor() as usize;
|
||||
|
||||
if start_visual_line == end_visual_line {
|
||||
return Some(Bounds::from_corners(
|
||||
point(
|
||||
bounds.left() + start_pos.x,
|
||||
bounds.top() + line.y_offset + start_pos.y,
|
||||
),
|
||||
point(
|
||||
bounds.left() + end_pos.x,
|
||||
bounds.top() + line.y_offset + start_pos.y + self.line_height(),
|
||||
),
|
||||
));
|
||||
} else {
|
||||
return Some(Bounds::from_corners(
|
||||
point(
|
||||
bounds.left() + start_pos.x,
|
||||
bounds.top() + line.y_offset + start_pos.y,
|
||||
),
|
||||
point(
|
||||
bounds.left() + wrapped.width(),
|
||||
bounds.top() + line.y_offset + start_pos.y + self.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);
|
||||
Some(self.content().utf_offset_8to16(index))
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
use crate::input::unicode::UnicodeString;
|
||||
use std::ops::Range;
|
||||
|
||||
pub trait InputStorage: UnicodeString {
|
||||
fn len(&self) -> usize;
|
||||
fn as_str(&self) -> &str;
|
||||
fn emplace(&mut self, s: &str);
|
||||
fn update_utf8(&mut self, range_utf8: Range<usize>, text: &str);
|
||||
fn replace_range(&mut self, range: Range<usize>, text: &str);
|
||||
}
|
||||
|
||||
/// A light wrapper around std String as a storage medium for `InputState`.
|
||||
#[derive(Default)]
|
||||
pub struct Standard {
|
||||
inner: String,
|
||||
/// Cached UTF-16 length of content for faster IME operations. Lazily computed when queried.
|
||||
cached_utf16_len: Option<usize>,
|
||||
}
|
||||
impl std::ops::Deref for Standard {
|
||||
type Target = String;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
impl std::ops::DerefMut for Standard {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.inner
|
||||
}
|
||||
}
|
||||
impl UnicodeString for Standard {
|
||||
fn len_utf16_cached(&self) -> Option<usize> {
|
||||
self.cached_utf16_len
|
||||
}
|
||||
|
||||
fn content_utf8(&self) -> &str {
|
||||
&self.inner
|
||||
}
|
||||
|
||||
fn len_utf16(&self) -> usize {
|
||||
if let Some(len) = self.cached_utf16_len {
|
||||
return len;
|
||||
}
|
||||
self.inner.chars().map(|c| c.len_utf16()).sum()
|
||||
}
|
||||
|
||||
fn clear_utf16_cache(&mut self) {
|
||||
self.cached_utf16_len = None;
|
||||
}
|
||||
}
|
||||
impl InputStorage for Standard {
|
||||
fn len(&self) -> usize {
|
||||
self.inner.len()
|
||||
}
|
||||
|
||||
fn as_str(&self) -> &str {
|
||||
self.inner.as_str()
|
||||
}
|
||||
|
||||
fn emplace(&mut self, s: &str) {
|
||||
self.inner = s.to_owned();
|
||||
self.cached_utf16_len = None;
|
||||
}
|
||||
|
||||
fn update_utf8(&mut self, range_utf8: Range<usize>, text: &str) {
|
||||
if let Some(cached_len) = self.cached_utf16_len {
|
||||
let removed_utf16_len: usize =
|
||||
self.inner[range_utf8].chars().map(|c| c.len_utf16()).sum();
|
||||
let added_utf16_len: usize = text.chars().map(|c| c.len_utf16()).sum();
|
||||
self.cached_utf16_len = Some(cached_len - removed_utf16_len + added_utf16_len);
|
||||
}
|
||||
}
|
||||
|
||||
fn replace_range(&mut self, range: Range<usize>, text: &str) {
|
||||
self.inner.replace_range(range, &text);
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
use std::ops::Range;
|
||||
|
||||
pub trait UnicodeString {
|
||||
fn len_utf16_cached(&self) -> Option<usize>;
|
||||
/// Returns a reference to the utf8 string.
|
||||
fn content_utf8(&self) -> &str;
|
||||
/// Returns the UTF-16 length of the content.
|
||||
fn len_utf16(&self) -> usize;
|
||||
|
||||
fn clear_utf16_cache(&mut self) {}
|
||||
|
||||
fn utf_offset_8to16(&self, pos_uft8: usize) -> usize {
|
||||
// Fast path: if offset is 0, return 0
|
||||
if pos_uft8 == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Fast path: if offset is at or past end, return cached length
|
||||
if pos_uft8 >= self.content_utf8().len() {
|
||||
return self.len_utf16();
|
||||
}
|
||||
|
||||
let mut pos_utf16 = 0;
|
||||
let mut counter_utf8 = 0;
|
||||
|
||||
for character in self.content_utf8().chars() {
|
||||
if counter_utf8 >= pos_uft8 {
|
||||
break;
|
||||
}
|
||||
counter_utf8 += character.len_utf8();
|
||||
pos_utf16 += character.len_utf16();
|
||||
}
|
||||
|
||||
pos_utf16
|
||||
}
|
||||
|
||||
fn utf_offset_16to8(&self, pos_utf16: usize) -> usize {
|
||||
// Fast path: if offset is 0, return 0
|
||||
if pos_utf16 == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Fast path: if we have cached length and offset is at or past end
|
||||
if let Some(utf16_len) = self.len_utf16_cached() {
|
||||
if pos_utf16 >= utf16_len {
|
||||
return self.content_utf8().len();
|
||||
}
|
||||
}
|
||||
|
||||
let mut pos_utf8 = 0;
|
||||
let mut counter_utf16 = 0;
|
||||
|
||||
for character in self.content_utf8().chars() {
|
||||
if counter_utf16 >= pos_utf16 {
|
||||
break;
|
||||
}
|
||||
counter_utf16 += character.len_utf16();
|
||||
pos_utf8 += character.len_utf8();
|
||||
}
|
||||
|
||||
pos_utf8.min(self.content_utf8().len())
|
||||
}
|
||||
|
||||
fn utf_range_8to16(&self, range_utf8: &Range<usize>) -> Range<usize> {
|
||||
self.utf_offset_8to16(range_utf8.start)..self.utf_offset_8to16(range_utf8.end)
|
||||
}
|
||||
|
||||
fn utf_range_16to8(&self, range_utf16: &Range<usize>) -> Range<usize> {
|
||||
self.utf_offset_16to8(range_utf16.start)..self.utf_offset_16to8(range_utf16.end)
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1 @@
|
||||
pub mod editable_text;
|
||||
pub mod input;
|
||||
|
||||
Reference in New Issue
Block a user