Add overflow scroll support to Input. (#37)

Continue #27

- Fix cut and paste invalid replace range.
- Add `Shift-Home`, `Shift-End` for select to home or end.

## Show case


https://github.com/user-attachments/assets/d99c56e3-5241-46e2-b3ac-f72ef962b0b5
This commit is contained in:
Jason Lee 2024-07-17 10:31:17 +08:00 committed by GitHub
parent 07e1296916
commit 702863eb83
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 67 additions and 7 deletions

View file

@ -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
});

View file

@ -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<Range<usize>>,
last_layout: Option<ShapedLine>,
last_bounds: Option<Bounds<Pixels>>,
scroll_offset: Point<Pixels>,
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>) {
self.select_to(0, cx);
}
fn select_to_end(&mut self, _: &SelectToEnd, cx: &mut ViewContext<Self>) {
self.select_to(self.text.len(), cx);
}
fn backspace(&mut self, _: &Backspace, cx: &mut ViewContext<Self>) {
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<Self>) {
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>) {
self.selected_range = offset..offset;
self.pause_blink_cursor(cx);
cx.notify()
}
@ -567,9 +582,11 @@ struct TextElement {
input: View<TextInput>,
}
struct PrepaintState {
scroll_offset: Point<Pixels>,
line: Option<ShapedLine>,
cursor: Option<PaintQuad>,
selection: Option<PaintQuad>,
bounds: Bounds<Pixels>,
}
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<Pixels>,
_: Bounds<Pixels>,
_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()