diff --git a/crates/story/src/input_story.rs b/crates/story/src/input_story.rs index 47ceed54..b0a8941d 100644 --- a/crates/story/src/input_story.rs +++ b/crates/story/src/input_story.rs @@ -38,7 +38,10 @@ impl InputStory { fn new(cx: &mut WindowContext) -> Self { let input1 = cx.new_view(|cx| { let mut input = TextInput::new(cx).cleanable(true); - input.set_text("Hello 世界", cx); + input.set_text( + "Hello 世界,this is GPUI component, this is a long text.", + cx, + ); input }); diff --git a/crates/ui/src/input.rs b/crates/ui/src/input.rs index e9533482..5a691242 100644 --- a/crates/ui/src/input.rs +++ b/crates/ui/src/input.rs @@ -11,15 +11,15 @@ use crate::theme::ActiveTheme; use crate::{event::InterativeElementExt as _, Size}; use crate::{Clickable, IconName, StyledExt as _}; use blink_cursor::BlinkCursor; +use gpui::prelude::FluentBuilder as _; use gpui::{ - actions, div, fill, point, prelude, px, relative, rems, size, AnyView, AppContext, Bounds, - ClickEvent, ClipboardItem, Context as _, Element, ElementId, ElementInputHandler, EventEmitter, + actions, div, fill, point, px, relative, rems, size, AnyView, AppContext, Bounds, ClickEvent, + ClipboardItem, Context as _, Element, ElementId, ElementInputHandler, EventEmitter, FocusHandle, FocusableView, GlobalElementId, InteractiveElement as _, IntoElement, KeyBinding, KeyDownEvent, LayoutId, Model, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement as _, Pixels, Point, Render, ShapedLine, SharedString, Style, Styled as _, TextRun, UnderlineStyle, View, ViewContext, ViewInputHandler, WindowContext, }; -use prelude::FluentBuilder as _; use unicode_segmentation::*; mod blink_cursor; @@ -37,6 +37,8 @@ actions!( SelectAll, Home, End, + SelectToHome, + SelectToEnd, ShowCharacterPalette, Copy, Cut, @@ -63,6 +65,8 @@ pub fn init(cx: &mut AppContext) { KeyBinding::new("shift-right", SelectRight, None), KeyBinding::new("home", Home, None), KeyBinding::new("end", End, None), + KeyBinding::new("shift-home", SelectToHome, None), + KeyBinding::new("shift-end", SelectToEnd, None), #[cfg(target_os = "macos")] KeyBinding::new("ctrl-cmd-space", ShowCharacterPalette, None), #[cfg(target_os = "macos")] @@ -100,6 +104,7 @@ pub struct TextInput { marked_range: Option>, last_layout: Option, last_bounds: Option>, + scroll_offset: Point, is_selecting: bool, disabled: bool, masked: bool, @@ -124,6 +129,7 @@ impl TextInput { marked_range: None, last_layout: None, last_bounds: None, + scroll_offset: point(px(0.), px(0.)), is_selecting: false, disabled: false, masked: false, @@ -257,6 +263,14 @@ impl TextInput { self.move_to(self.text.len(), cx); } + fn select_to_home(&mut self, _: &SelectToHome, cx: &mut ViewContext) { + self.select_to(0, cx); + } + + fn select_to_end(&mut self, _: &SelectToEnd, cx: &mut ViewContext) { + self.select_to(self.text.len(), cx); + } + fn backspace(&mut self, _: &Backspace, cx: &mut ViewContext) { if self.selected_range.is_empty() { self.select_to(self.previous_boundary(self.cursor_offset()), cx) @@ -321,18 +335,19 @@ impl TextInput { let selected_text = self.text[self.selected_range.clone()].to_string(); cx.write_to_clipboard(ClipboardItem::new(selected_text)); - self.replace_text_in_range(Some(self.selected_range.clone()), "", cx); + self.replace_text_in_range(None, "", cx); } fn paste(&mut self, _: &Paste, cx: &mut ViewContext) { if let Some(clipboard) = cx.read_from_clipboard() { let new_text = clipboard.text().replace('\n', ""); - self.replace_text_in_range(Some(self.selected_range.clone()), &new_text, cx); + self.replace_text_in_range(None, &new_text, cx); } } fn move_to(&mut self, offset: usize, cx: &mut ViewContext) { self.selected_range = offset..offset; + self.pause_blink_cursor(cx); cx.notify() } @@ -567,9 +582,11 @@ struct TextElement { input: View, } struct PrepaintState { + scroll_offset: Point, line: Option, cursor: Option, selection: Option, + bounds: Bounds, } impl IntoElement for TextElement { @@ -666,9 +683,41 @@ impl Element for TextElement { .shape_line(disaplay_text, font_size, &runs) .unwrap(); + // Calculate the scroll offset to keep the cursor in view + let mut scroll_offset = input.scroll_offset; + let mut bounds = bounds; + let right_margin = px(5.); let cursor_pos = line.x_for_index(cursor); + let cursor_start = line.x_for_index(selected_range.start); + let cursor_end = line.x_for_index(selected_range.end); + + scroll_offset.x = if scroll_offset.x + cursor_pos > (bounds.size.width - right_margin) { + // cursor is out of right + bounds.size.width - right_margin - cursor_pos + } else if scroll_offset.x + cursor_pos < px(0.) { + // cursor is out of left + scroll_offset.x - cursor_pos + } else { + scroll_offset.x + }; + + if input.selection_reversed { + if scroll_offset.x + cursor_start < px(0.) { + // selection start is out of left + scroll_offset.x = -cursor_start; + } + } else { + if scroll_offset.x + cursor_end <= px(0.) { + // selection end is out of left + scroll_offset.x = -cursor_end; + } + } + + bounds.origin = bounds.origin + scroll_offset; + let inset = px(0.5); let (selection, cursor) = if selected_range.is_empty() && input.show_cursor(cx) { + // cursor blink ( None, Some(fill( @@ -680,6 +729,7 @@ impl Element for TextElement { )), ) } else { + // selection background ( Some(fill( Bounds::from_corners( @@ -699,6 +749,8 @@ impl Element for TextElement { }; PrepaintState { + scroll_offset, + bounds, line: Some(line), cursor, selection, @@ -708,13 +760,14 @@ impl Element for TextElement { fn paint( &mut self, _id: Option<&GlobalElementId>, - bounds: Bounds, + _: Bounds, _request_layout: &mut Self::RequestLayoutState, prepaint: &mut Self::PrepaintState, cx: &mut WindowContext, ) { let focus_handle = self.input.read(cx).focus_handle.clone(); let focused = focus_handle.is_focused(cx); + let bounds = prepaint.bounds; cx.handle_input( &focus_handle, @@ -732,6 +785,7 @@ impl Element for TextElement { } } self.input.update(cx, |input, _cx| { + input.scroll_offset = prepaint.scroll_offset; input.last_layout = Some(line); input.last_bounds = Some(bounds); }); @@ -756,6 +810,8 @@ impl Render for TextInput { .on_action(cx.listener(Self::select_left)) .on_action(cx.listener(Self::select_right)) .on_action(cx.listener(Self::select_all)) + .on_action(cx.listener(Self::select_to_home)) + .on_action(cx.listener(Self::select_to_end)) .on_action(cx.listener(Self::home)) .on_action(cx.listener(Self::end)) .on_action(cx.listener(Self::show_character_palette)) @@ -795,6 +851,7 @@ impl Render for TextInput { .items_center() .child( div() + .id("TextElement") .flex_grow() .overflow_x_hidden() .cursor_text()