From 4c7b51273cd77b128dcd2273c1b0c55535a5f8c7 Mon Sep 17 00:00:00 2001 From: temportalflux Date: Sat, 6 Jun 2026 18:29:55 -0400 Subject: [PATCH] add documentation --- crates/gpui_elements/src/input/cursor.rs | 15 ++- crates/gpui_elements/src/input/state.rs | 111 +++++++++++++---------- 2 files changed, 77 insertions(+), 49 deletions(-) diff --git a/crates/gpui_elements/src/input/cursor.rs b/crates/gpui_elements/src/input/cursor.rs index 8017ed9842..359bf76b5b 100644 --- a/crates/gpui_elements/src/input/cursor.rs +++ b/crates/gpui_elements/src/input/cursor.rs @@ -4,11 +4,24 @@ use std::time::Duration; /// Default interval for cursor blinking. pub const DEFAULT_BLINK_INTERVAL: Duration = Duration::from_millis(500); +/// Configuration for cursor blinking, to be provided to InputState. +pub enum CursorBlinkType<'app> { + /// The cursor will not blink. + Disabled, + /// The cursor will blink at some interval. + Enabled { + /// Provide the app so that the internal state to track cursor blinking can be created. + app: &'app mut gpui::App, + /// The interval to blink at. If none, the default value of 500ms is used (defined by `DEFAULT_BLINK_INTERVAL`). + interval: Option, + }, +} + /// Manages the blinking state of a text cursor. /// /// The cursor blinks at a configurable interval when enabled. Blinking can be /// temporarily paused (e.g., during typing) to provide immediate visual feedback. -pub struct CursorBlink { +pub(super) struct CursorBlink { interval: Duration, generation: usize, visible: bool, diff --git a/crates/gpui_elements/src/input/state.rs b/crates/gpui_elements/src/input/state.rs index e3173e8dc7..0537abe5c4 100644 --- a/crates/gpui_elements/src/input/state.rs +++ b/crates/gpui_elements/src/input/state.rs @@ -1,5 +1,5 @@ use super::actions::*; -use crate::input::InputLayoutStyle; +use crate::input::{CursorBlinkType, InputLayoutStyle}; use gpui::{ App, AppContext, ClipboardItem, Context, Entity, EntityId, EntityInputHandler, EventEmitter, FocusHandle, Focusable, NavigationDirection, Pixels, Point, SharedString, Size, Subscription, @@ -35,33 +35,50 @@ impl EventEmitter for InputState {} /// - Keyboard navigation and editing actions /// - IME (Input Method Editor) support via `EntityInputHandler` pub struct InputState { + /// The id of this entity (for app notifies when self context is unavailable) entity_id: EntityId, focus_handle: FocusHandle, + /// The true internal text content: String, + /// Cached UTF-16 length of content for faster IME operations. Lazily computed when queried. + pub(super) cached_utf16_len: Option, + + /// The style of layout (single or multiline). + pub(super) layout_style: InputLayoutStyle, + /// The utf-8 character range that is currently selected by the user. + /// NOTE: because each input has its own selection state, its trivial for users to have multiple selections active across multiple inputs at the same time. + /// This could be considered undesirable behavior, and doing so would prompt the question of should there be a mechanism to clear selection when focus is lost. pub(super) selected_range: Range, + /// The direction of the selection_range. Forward means providing in iteration order along `content`. Back means reverse iteration order. pub(super) selection_direction: NavigationDirection, + /// The utf-8 character range of `content` that is currently marked/highlighted. pub(super) marked_range: Option>, - pub(super) logical_lines: Vec, + // refreshed each update by the element, for conveinent access in mutations and painting pub(super) layout_data: InputLayoutData, - is_selecting: bool, - last_click_position: Option>, - click_count: usize, - /// Scroll offset - vertical for multiline, horizontal for single-line - pub(super) scroll_offset: Pixels, - pub(super) layout_style: InputLayoutStyle, - history_grouping_interval: Duration, - /// Stack of previous states for undo. - undo_stack: Vec, - /// Stack of undone states for redo. - redo_stack: Vec, - /// Optional entity and subscription tracking the blinking of the text cursor. - cursor_blink: Option<(Entity, Subscription)>, + /// A reinterpretation of `content` as wrapped lines with layout information. Regenerated when content changes or the layout changes during element painting. + pub(super) logical_lines: Vec, /// Tracks whether we were focused on the last update. was_focused: bool, - /// Cached UTF-16 length of content for faster IME operations. - /// Lazily computed when None. - pub(super) cached_utf16_len: Option, + + /// True while the user is in the act of highlighting a section of the text (e.g. during mouse pressed & dragging). + is_selecting: bool, + /// The last ui location relative to the element that the user clicked. Used to filter when a user clicks multiple times in the same area. + last_click_position: Option>, + /// The number of times the user has clicked `last_click_position`. Used to determine which click behavior to trigger, depending on single, double, or triple clicks. + click_count: usize, + /// The distance in pixels from the start of the text a user has scrolled along the layout_style axis (singleline is horizontal, multiline is vertical). + pub(super) scroll_offset: Pixels, + + /// The maximum duration between changes to `content` that can be grouped together as a single entry in the history log. + history_grouping_interval: Duration, + /// Stack of previous states for undo. + history_undo_stack: Vec, + /// Stack of undone states for redo. + history_redo_stack: Vec, + + /// Optional entity and subscription tracking the blinking of the text cursor. + cursor_blink: Option<(Entity, Subscription)>, } /// Data built during element prepaint that is stored in InputState for conveinence @@ -100,14 +117,6 @@ pub(super) struct InputLogicalLine { pub visual_line_count: usize, } -pub enum CursorBlinkType<'app> { - Disabled, - Enabled { - app: &'app mut App, - interval: Option, - }, -} - impl Focusable for InputState { fn focus_handle(&self, _: &App) -> FocusHandle { self.focus_handle.clone() @@ -123,23 +132,29 @@ impl InputState { entity_id: cx.entity_id(), focus_handle: cx.focus_handle(), content: String::default(), + cached_utf16_len: None, + + layout_style: InputLayoutStyle::SingleLine, selected_range: 0..0, selection_direction: NavigationDirection::Forward, marked_range: None, + layout_data: InputLayoutData::default(), logical_lines: Vec::new(), + was_focused: false, + is_selecting: false, last_click_position: None, click_count: 0, scroll_offset: px(0.), - layout_style: InputLayoutStyle::SingleLine, + history_grouping_interval: super::DEFAULT_GROUP_INTERVAL, - undo_stack: Vec::new(), - cached_utf16_len: None, - redo_stack: Vec::new(), + history_undo_stack: Vec::new(), + history_redo_stack: Vec::new(), + cursor_blink: None, - was_focused: false, }; + // TODO: This is unoptimal for non-blinking cases, since the entity is generated and then discarded. this = this.cursor_blink(CursorBlinkType::Enabled { app: cx, interval: None, @@ -176,8 +191,8 @@ impl InputState { self.selection_direction = NavigationDirection::Forward; self.marked_range = None; self.layout_data.dirty = true; - self.undo_stack.clear(); - self.redo_stack.clear(); + self.history_undo_stack.clear(); + self.history_redo_stack.clear(); self.cached_utf16_len = None; self.pause_cursor_blink(cx); cx.emit(InputStateEvent::TextChanged); @@ -283,12 +298,12 @@ impl InputState { /// Returns whether undo is available based on the recorded states. pub fn is_undo_available(&self) -> bool { - !self.undo_stack.is_empty() + !self.history_undo_stack.is_empty() } /// Returns whether redo is currently available based on the recorded states. pub fn is_redo_available(&self) -> bool { - !self.redo_stack.is_empty() + !self.history_redo_stack.is_empty() } /// Inserts text at the current cursor position, replacing any content that is currently selected (i.e. `selection_range` is non-empty). @@ -336,12 +351,12 @@ impl InputState { /// Reverts the last edit. pub fn undo_action(&mut self, cx: &mut Context) { - if let Some(entry) = self.undo_stack.pop() { + if let Some(entry) = self.history_undo_stack.pop() { let selected_range = entry.selected_range.clone(); let selection_direction = entry.selection_direction; let redo_entry = entry.apply_undo(&mut self.content); - self.redo_stack.push(redo_entry); + self.history_redo_stack.push(redo_entry); self.selected_range = selected_range; self.selection_direction = selection_direction; @@ -355,14 +370,14 @@ impl InputState { /// Restores the last edit reverted by `undo_action` (or the undo action binding). pub fn redo_action(&mut self, cx: &mut Context) { - if let Some(entry) = self.redo_stack.pop() { + if let Some(entry) = self.history_redo_stack.pop() { let undo_entry = entry.apply_redo(&mut self.content); let cursor_pos = undo_entry.range.start; self.selected_range = cursor_pos..cursor_pos; self.selection_direction = NavigationDirection::Forward; - self.undo_stack.push(undo_entry); + self.history_undo_stack.push(undo_entry); self.layout_data.dirty = true; self.cached_utf16_len = None; self.scroll_to_cursor(); @@ -375,14 +390,14 @@ impl InputState { // Action implementations impl InputState { pub(super) fn undo(&mut self, _: &Undo, _: &mut Window, cx: &mut Context) { - if let Some(entry) = self.undo_stack.pop() { + if let Some(entry) = self.history_undo_stack.pop() { // Remember selection to restore let selected_range = entry.selected_range.clone(); let selection_direction = entry.selection_direction; // Apply the undo patch and get the redo patch let redo_entry = entry.apply_undo(&mut self.content); - self.redo_stack.push(redo_entry); + self.history_redo_stack.push(redo_entry); // Restore selection state self.selected_range = selected_range; @@ -396,7 +411,7 @@ impl InputState { } pub(super) fn redo(&mut self, _: &Redo, _: &mut Window, cx: &mut Context) { - if let Some(entry) = self.redo_stack.pop() { + if let Some(entry) = self.history_redo_stack.pop() { // Apply the redo patch and get the undo patch let undo_entry = entry.apply_redo(&mut self.content); @@ -406,7 +421,7 @@ impl InputState { self.selected_range = cursor_pos..cursor_pos; self.selection_direction = NavigationDirection::Forward; - self.undo_stack.push(undo_entry); + self.history_undo_stack.push(undo_entry); self.layout_data.dirty = true; self.cached_utf16_len = None; self.scroll_to_cursor(); @@ -825,7 +840,7 @@ impl InputState { let now = Instant::now(); // Check if we should group with the last entry - if let Some(last) = self.undo_stack.last() { + if let Some(last) = self.history_undo_stack.last() { if now.duration_since(last.timestamp) < self.history_grouping_interval { // Within group interval - extend the existing patch // We need to merge this edit with the previous one @@ -836,7 +851,7 @@ impl InputState { // Capture the text that will be replaced let old_text = self.content[range.clone()].to_string(); - self.undo_stack.push(super::HistoryEntry { + self.history_undo_stack.push(super::HistoryEntry { range: range.start..range.start + new_text_len, old_text, new_text_len, @@ -846,12 +861,12 @@ impl InputState { }); // Limit history size - if self.undo_stack.len() > super::MAX_HISTORY_LEN { - self.undo_stack.remove(0); + if self.history_undo_stack.len() > super::MAX_HISTORY_LEN { + self.history_undo_stack.remove(0); } // New edit invalidates redo stack - self.redo_stack.clear(); + self.history_redo_stack.clear(); } /// Returns the utf-8 character position of first character after the first new-line preceeding the character at the provided utf-8 character position.