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:
parent
07e1296916
commit
702863eb83
2 changed files with 67 additions and 7 deletions
|
|
@ -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
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Reference in a new issue