remove previous input iteration

This commit is contained in:
temportalflux
2026-07-11 09:31:07 -04:00
parent de19ea0d6d
commit 5e6d3028e0
12 changed files with 0 additions and 4645 deletions
-25
View File
@@ -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))
}
-21
View File
@@ -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),
}
}
}
-282
View File
@@ -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,
));
}
}
-205
View File
@@ -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
}
}
-61
View File
@@ -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)
}
}
-24
View File
@@ -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,
}
}
}
-685
View File
@@ -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))
}
}
-77
View File
@@ -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);
}
}
-71
View File
@@ -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
View File
@@ -1,2 +1 @@
pub mod editable_text;
pub mod input;