diff --git a/crates/gpui_elements/src/input/paint.rs b/crates/gpui_elements/src/input/paint.rs index 6a86ae721a..28ca4846a2 100644 --- a/crates/gpui_elements/src/input/paint.rs +++ b/crates/gpui_elements/src/input/paint.rs @@ -191,7 +191,7 @@ struct InputStateSnapshot { marked_range: Option>, cursor_position: usize, logical_lines: Vec, - scroll_offset: Pixels, + scroll_distance: Pixels, line_height: Pixels, } impl InputStateSnapshot { @@ -201,7 +201,7 @@ impl InputStateSnapshot { let marked_range = input_state.marked_range().cloned(); let cursor_position = input_state.cursor_position(); let logical_lines = input_state.logical_lines.clone(); - let scroll_offset = input_state.scroll_offset; + 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!( @@ -216,7 +216,7 @@ impl InputStateSnapshot { marked_range, cursor_position, logical_lines, - scroll_offset, + scroll_distance, line_height, } } @@ -241,6 +241,7 @@ impl<'app> PaintContext<'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| { @@ -257,7 +258,7 @@ impl<'app> PaintContext<'app> { 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 + input.scroll_offset); + .apply_along(axis, |pos| pos + scroll_distance); input.on_mouse_down( text_position, event.click_count, @@ -293,7 +294,7 @@ impl<'app> PaintContext<'app> { 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 + input.scroll_offset); + .apply_along(axis, |pos| pos + scroll_distance); input.on_mouse_move(text_position, cx); }); } @@ -330,7 +331,7 @@ impl<'app> PaintContext<'app> { } } }; - input.scroll_offset = (input.scroll_offset - delta).clamp(px(0.), max_scroll); + input.apply_scroll_delta(delta, max_scroll); cx.notify(); }); } @@ -372,7 +373,7 @@ impl<'app> PaintContext<'app> { 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_offset; + let line_y = line.y_offset - self.snapshot.scroll_distance; if !one_line { if !self.is_line_visible(line) { @@ -442,7 +443,7 @@ impl<'app> PaintContext<'app> { 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_offset; + let line_y = line_layout.y_offset - self.snapshot.scroll_distance; if !self.is_line_visible(line_layout) { continue; @@ -499,7 +500,7 @@ impl<'app> PaintContext<'app> { fn find_cursor_position_in_layouts(&self) -> Point { for line in &self.snapshot.logical_lines { - let line_y = line.y_offset - self.snapshot.scroll_offset; + let line_y = line.y_offset - self.snapshot.scroll_distance; if !self.is_line_visible(line) { continue; @@ -544,7 +545,7 @@ impl<'app> PaintContext<'app> { } fn is_line_visible(&self, line: &InputLogicalLine) -> bool { - let line_y = line.y_offset - self.snapshot.scroll_offset; + 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 } @@ -565,7 +566,7 @@ impl<'app> PaintContext<'app> { return; }; - let line_y = line.y_offset - self.snapshot.scroll_offset; + let line_y = line.y_offset - self.snapshot.scroll_distance; let line_start = line.text_range.start; let line_end = line.text_range.end; diff --git a/crates/gpui_elements/src/input/state.rs b/crates/gpui_elements/src/input/state.rs index d94b38c3b1..d3346d4da4 100644 --- a/crates/gpui_elements/src/input/state.rs +++ b/crates/gpui_elements/src/input/state.rs @@ -44,15 +44,15 @@ pub struct InputState { cached_utf16_len: Option, /// The style of layout (single or multiline). - pub(super) layout_style: InputLayoutStyle, + 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, + 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, + selection_direction: NavigationDirection, /// The utf-8 character range of `content` that is currently marked/highlighted. - pub(super) marked_range: Option>, + marked_range: Option>, // refreshed each update by the element, for conveinent access in mutations and painting pub(super) layout_data: InputLayoutData, @@ -68,7 +68,7 @@ pub struct InputState { /// 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, + scroll_distance: 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, @@ -163,7 +163,7 @@ impl InputState { is_selecting: false, last_click_position: None, click_count: 0, - scroll_offset: px(0.), + scroll_distance: px(0.), history_grouping_interval: super::DEFAULT_GROUP_INTERVAL, history_undo_stack: Vec::new(), @@ -232,6 +232,10 @@ impl InputState { &self.selected_range } + pub fn selection_direction(&self) -> NavigationDirection { + self.selection_direction + } + /// Sets the selection range directly. pub fn set_selected_range(&mut self, range: Range) { let range = range.start.min(self.content.len())..range.end.min(self.content.len()); @@ -254,7 +258,7 @@ impl InputState { /// Returns true if the scroll position is at the top. pub fn at_top(&self) -> bool { - self.scroll_offset <= px(0.) + self.scroll_distance <= px(0.) } /// Returns true if the scroll position is at the bottom. @@ -266,7 +270,7 @@ impl InputState { return true; } - self.scroll_offset + visible_height >= content_height + self.scroll_distance + visible_height >= content_height } /// Returns the scroll progress as a value from 0.0 (top) to 1.0 (bottom). @@ -279,12 +283,12 @@ impl InputState { return 0.0; } - (self.scroll_offset / max_scroll).clamp(0.0, 1.0) + (self.scroll_distance / max_scroll).clamp(0.0, 1.0) } /// Returns how far the content is scrolled from the top in pixels. pub fn distance_from_top(&self) -> Pixels { - self.scroll_offset.max(px(0.)) + self.scroll_distance.max(px(0.)) } /// Returns how far the content is from the bottom in pixels. @@ -297,7 +301,7 @@ impl InputState { return px(0.); } - (max_scroll - self.scroll_offset).max(px(0.)) + (max_scroll - self.scroll_distance).max(px(0.)) } /// Configures how long the input will wait between user-input changes to create new logs in the history for undo/redo. @@ -347,7 +351,7 @@ impl InputState { self.cached_utf16_len = Some(cached_len - removed_utf16_len + added_utf16_len); } - self.replace_range(range.clone(), &text_to_insert); + self.replace_text_at_range(range.clone(), &text_to_insert); self.selected_range = range.start + text_to_insert.len()..range.start + text_to_insert.len(); @@ -834,8 +838,12 @@ impl InputState { self.layout_data.line_height } + pub(super) fn set_marked_range(&mut self, range: Option>) { + self.marked_range = range; + } + /// Replaces the provided utf-8 character range with the provided text - pub(super) fn replace_range(&mut self, range: Range, text: &str) { + pub(super) fn replace_text_at_range(&mut self, range: Range, text: &str) { self.content.replace_range(range, &text); } @@ -944,6 +952,10 @@ impl InputState { self.content.len() } + pub(super) fn apply_scroll_delta(&mut self, delta: Pixels, max: Pixels) { + self.scroll_distance = (self.scroll_distance - delta).clamp(px(0.), max); + } + pub(super) fn scroll_to_cursor(&mut self) { if self.logical_lines.is_empty() { return; @@ -971,19 +983,20 @@ impl InputState { px(0.) }; - let visible_left = self.scroll_offset; - let visible_right = self.scroll_offset + self.layout_data.available_size.width; + let visible_left = self.scroll_distance; + let visible_right = self.scroll_distance + self.layout_data.available_size.width; // Add some padding so cursor isn't right at the edge let padding = px(2.0); if cursor_x < visible_left + padding { - self.scroll_offset = (cursor_x - padding).max(px(0.)); + self.scroll_distance = (cursor_x - padding).max(px(0.)); } else if cursor_x > visible_right - padding { - self.scroll_offset = cursor_x - self.layout_data.available_size.width + padding; + self.scroll_distance = + cursor_x - self.layout_data.available_size.width + padding; } - self.scroll_offset = self.scroll_offset.max(px(0.)); + self.scroll_distance = self.scroll_distance.max(px(0.)); } InputLayoutStyle::MultiLine => { if self.layout_data.available_size.height <= px(0.) { @@ -1015,18 +1028,18 @@ impl InputState { line.y_offset }; - let visible_top = self.scroll_offset; + let visible_top = self.scroll_distance; let visible_bottom = - self.scroll_offset + self.layout_data.available_size.height; + self.scroll_distance + self.layout_data.available_size.height; if cursor_visual_y < visible_top { - self.scroll_offset = cursor_visual_y; + self.scroll_distance = cursor_visual_y; } else if cursor_visual_y + line_height > visible_bottom { - self.scroll_offset = (cursor_visual_y + line_height) + self.scroll_distance = (cursor_visual_y + line_height) - self.layout_data.available_size.height; } - self.scroll_offset = self.scroll_offset.max(px(0.)); + self.scroll_distance = self.scroll_distance.max(px(0.)); break; } } diff --git a/crates/gpui_elements/src/input/state_input_handler.rs b/crates/gpui_elements/src/input/state_input_handler.rs index 0af1b83792..855772916f 100644 --- a/crates/gpui_elements/src/input/state_input_handler.rs +++ b/crates/gpui_elements/src/input/state_input_handler.rs @@ -28,8 +28,8 @@ impl EntityInputHandler for super::InputState { _cx: &mut Context, ) -> Option { Some(UTF16Selection { - range: self.utf_range_8to16(&self.selected_range), - reversed: self.selection_direction == NavigationDirection::Back, + range: self.utf_range_8to16(self.selected_range()), + reversed: self.selection_direction() == NavigationDirection::Back, }) } @@ -38,13 +38,13 @@ impl EntityInputHandler for super::InputState { _window: &mut Window, _cx: &mut Context, ) -> Option> { - self.marked_range + self.marked_range() .as_ref() .map(|range| self.utf_range_8to16(range)) } fn unmark_text(&mut self, _window: &mut Window, _cx: &mut Context) { - self.marked_range = None; + self.set_marked_range(None); } fn replace_text_in_range( @@ -57,21 +57,21 @@ impl EntityInputHandler for super::InputState { let range = range_utf16 .as_ref() .map(|range_utf16| self.utf_range_16to8(range_utf16)) - .or(self.marked_range.clone()) - .unwrap_or(self.selected_range.clone()); + .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); + 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.push_undo_patch(range.clone(), text_to_insert.len()); - self.update_utf16_len(range.clone(), &text_to_insert); - self.replace_range(range.clone(), &text_to_insert); - self.selected_range = - range.start + text_to_insert.len()..range.start + text_to_insert.len(); - self.marked_range.take(); + 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.layout_data.dirty = true; self.pause_cursor_blink(cx); @@ -90,28 +90,27 @@ impl EntityInputHandler for super::InputState { let range = range_utf16 .as_ref() .map(|range_utf16| self.utf_range_16to8(range_utf16)) - .or(self.marked_range.clone()) - .unwrap_or(self.selected_range.clone()); + .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); + let text_to_insert = self.layout_style().sanitize_content(new_text); self.update_utf16_len(range.clone(), &text_to_insert); - self.replace_range(range.clone(), &text_to_insert); - - if !text_to_insert.is_empty() { - self.marked_range = Some(range.start..range.start + text_to_insert.len()); - } else { - self.marked_range = None; - } - - self.selected_range = new_selected_range_utf16 - .as_ref() - .map(|range_utf16| self.utf_range_16to8(range_utf16)) - .map(|new_range| new_range.start + range.start..new_range.end + range.start) - .unwrap_or_else(|| { + 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.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.layout_data.dirty = true; cx.emit(InputStateEvent::TextChanged);