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 {
|
fn new(cx: &mut WindowContext) -> Self {
|
||||||
let input1 = cx.new_view(|cx| {
|
let input1 = cx.new_view(|cx| {
|
||||||
let mut input = TextInput::new(cx).cleanable(true);
|
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
|
input
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,15 +11,15 @@ use crate::theme::ActiveTheme;
|
||||||
use crate::{event::InterativeElementExt as _, Size};
|
use crate::{event::InterativeElementExt as _, Size};
|
||||||
use crate::{Clickable, IconName, StyledExt as _};
|
use crate::{Clickable, IconName, StyledExt as _};
|
||||||
use blink_cursor::BlinkCursor;
|
use blink_cursor::BlinkCursor;
|
||||||
|
use gpui::prelude::FluentBuilder as _;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, div, fill, point, prelude, px, relative, rems, size, AnyView, AppContext, Bounds,
|
actions, div, fill, point, px, relative, rems, size, AnyView, AppContext, Bounds, ClickEvent,
|
||||||
ClickEvent, ClipboardItem, Context as _, Element, ElementId, ElementInputHandler, EventEmitter,
|
ClipboardItem, Context as _, Element, ElementId, ElementInputHandler, EventEmitter,
|
||||||
FocusHandle, FocusableView, GlobalElementId, InteractiveElement as _, IntoElement, KeyBinding,
|
FocusHandle, FocusableView, GlobalElementId, InteractiveElement as _, IntoElement, KeyBinding,
|
||||||
KeyDownEvent, LayoutId, Model, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
|
KeyDownEvent, LayoutId, Model, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
|
||||||
PaintQuad, ParentElement as _, Pixels, Point, Render, ShapedLine, SharedString, Style,
|
PaintQuad, ParentElement as _, Pixels, Point, Render, ShapedLine, SharedString, Style,
|
||||||
Styled as _, TextRun, UnderlineStyle, View, ViewContext, ViewInputHandler, WindowContext,
|
Styled as _, TextRun, UnderlineStyle, View, ViewContext, ViewInputHandler, WindowContext,
|
||||||
};
|
};
|
||||||
use prelude::FluentBuilder as _;
|
|
||||||
use unicode_segmentation::*;
|
use unicode_segmentation::*;
|
||||||
|
|
||||||
mod blink_cursor;
|
mod blink_cursor;
|
||||||
|
|
@ -37,6 +37,8 @@ actions!(
|
||||||
SelectAll,
|
SelectAll,
|
||||||
Home,
|
Home,
|
||||||
End,
|
End,
|
||||||
|
SelectToHome,
|
||||||
|
SelectToEnd,
|
||||||
ShowCharacterPalette,
|
ShowCharacterPalette,
|
||||||
Copy,
|
Copy,
|
||||||
Cut,
|
Cut,
|
||||||
|
|
@ -63,6 +65,8 @@ pub fn init(cx: &mut AppContext) {
|
||||||
KeyBinding::new("shift-right", SelectRight, None),
|
KeyBinding::new("shift-right", SelectRight, None),
|
||||||
KeyBinding::new("home", Home, None),
|
KeyBinding::new("home", Home, None),
|
||||||
KeyBinding::new("end", End, None),
|
KeyBinding::new("end", End, None),
|
||||||
|
KeyBinding::new("shift-home", SelectToHome, None),
|
||||||
|
KeyBinding::new("shift-end", SelectToEnd, None),
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
KeyBinding::new("ctrl-cmd-space", ShowCharacterPalette, None),
|
KeyBinding::new("ctrl-cmd-space", ShowCharacterPalette, None),
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
|
|
@ -100,6 +104,7 @@ pub struct TextInput {
|
||||||
marked_range: Option<Range<usize>>,
|
marked_range: Option<Range<usize>>,
|
||||||
last_layout: Option<ShapedLine>,
|
last_layout: Option<ShapedLine>,
|
||||||
last_bounds: Option<Bounds<Pixels>>,
|
last_bounds: Option<Bounds<Pixels>>,
|
||||||
|
scroll_offset: Point<Pixels>,
|
||||||
is_selecting: bool,
|
is_selecting: bool,
|
||||||
disabled: bool,
|
disabled: bool,
|
||||||
masked: bool,
|
masked: bool,
|
||||||
|
|
@ -124,6 +129,7 @@ impl TextInput {
|
||||||
marked_range: None,
|
marked_range: None,
|
||||||
last_layout: None,
|
last_layout: None,
|
||||||
last_bounds: None,
|
last_bounds: None,
|
||||||
|
scroll_offset: point(px(0.), px(0.)),
|
||||||
is_selecting: false,
|
is_selecting: false,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
masked: false,
|
masked: false,
|
||||||
|
|
@ -257,6 +263,14 @@ impl TextInput {
|
||||||
self.move_to(self.text.len(), cx);
|
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>) {
|
fn backspace(&mut self, _: &Backspace, cx: &mut ViewContext<Self>) {
|
||||||
if self.selected_range.is_empty() {
|
if self.selected_range.is_empty() {
|
||||||
self.select_to(self.previous_boundary(self.cursor_offset()), cx)
|
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();
|
let selected_text = self.text[self.selected_range.clone()].to_string();
|
||||||
cx.write_to_clipboard(ClipboardItem::new(selected_text));
|
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>) {
|
fn paste(&mut self, _: &Paste, cx: &mut ViewContext<Self>) {
|
||||||
if let Some(clipboard) = cx.read_from_clipboard() {
|
if let Some(clipboard) = cx.read_from_clipboard() {
|
||||||
let new_text = clipboard.text().replace('\n', "");
|
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>) {
|
fn move_to(&mut self, offset: usize, cx: &mut ViewContext<Self>) {
|
||||||
self.selected_range = offset..offset;
|
self.selected_range = offset..offset;
|
||||||
|
self.pause_blink_cursor(cx);
|
||||||
cx.notify()
|
cx.notify()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -567,9 +582,11 @@ struct TextElement {
|
||||||
input: View<TextInput>,
|
input: View<TextInput>,
|
||||||
}
|
}
|
||||||
struct PrepaintState {
|
struct PrepaintState {
|
||||||
|
scroll_offset: Point<Pixels>,
|
||||||
line: Option<ShapedLine>,
|
line: Option<ShapedLine>,
|
||||||
cursor: Option<PaintQuad>,
|
cursor: Option<PaintQuad>,
|
||||||
selection: Option<PaintQuad>,
|
selection: Option<PaintQuad>,
|
||||||
|
bounds: Bounds<Pixels>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoElement for TextElement {
|
impl IntoElement for TextElement {
|
||||||
|
|
@ -666,9 +683,41 @@ impl Element for TextElement {
|
||||||
.shape_line(disaplay_text, font_size, &runs)
|
.shape_line(disaplay_text, font_size, &runs)
|
||||||
.unwrap();
|
.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_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 inset = px(0.5);
|
||||||
let (selection, cursor) = if selected_range.is_empty() && input.show_cursor(cx) {
|
let (selection, cursor) = if selected_range.is_empty() && input.show_cursor(cx) {
|
||||||
|
// cursor blink
|
||||||
(
|
(
|
||||||
None,
|
None,
|
||||||
Some(fill(
|
Some(fill(
|
||||||
|
|
@ -680,6 +729,7 @@ impl Element for TextElement {
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
// selection background
|
||||||
(
|
(
|
||||||
Some(fill(
|
Some(fill(
|
||||||
Bounds::from_corners(
|
Bounds::from_corners(
|
||||||
|
|
@ -699,6 +749,8 @@ impl Element for TextElement {
|
||||||
};
|
};
|
||||||
|
|
||||||
PrepaintState {
|
PrepaintState {
|
||||||
|
scroll_offset,
|
||||||
|
bounds,
|
||||||
line: Some(line),
|
line: Some(line),
|
||||||
cursor,
|
cursor,
|
||||||
selection,
|
selection,
|
||||||
|
|
@ -708,13 +760,14 @@ impl Element for TextElement {
|
||||||
fn paint(
|
fn paint(
|
||||||
&mut self,
|
&mut self,
|
||||||
_id: Option<&GlobalElementId>,
|
_id: Option<&GlobalElementId>,
|
||||||
bounds: Bounds<Pixels>,
|
_: Bounds<Pixels>,
|
||||||
_request_layout: &mut Self::RequestLayoutState,
|
_request_layout: &mut Self::RequestLayoutState,
|
||||||
prepaint: &mut Self::PrepaintState,
|
prepaint: &mut Self::PrepaintState,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
) {
|
) {
|
||||||
let focus_handle = self.input.read(cx).focus_handle.clone();
|
let focus_handle = self.input.read(cx).focus_handle.clone();
|
||||||
let focused = focus_handle.is_focused(cx);
|
let focused = focus_handle.is_focused(cx);
|
||||||
|
let bounds = prepaint.bounds;
|
||||||
|
|
||||||
cx.handle_input(
|
cx.handle_input(
|
||||||
&focus_handle,
|
&focus_handle,
|
||||||
|
|
@ -732,6 +785,7 @@ impl Element for TextElement {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.input.update(cx, |input, _cx| {
|
self.input.update(cx, |input, _cx| {
|
||||||
|
input.scroll_offset = prepaint.scroll_offset;
|
||||||
input.last_layout = Some(line);
|
input.last_layout = Some(line);
|
||||||
input.last_bounds = Some(bounds);
|
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_left))
|
||||||
.on_action(cx.listener(Self::select_right))
|
.on_action(cx.listener(Self::select_right))
|
||||||
.on_action(cx.listener(Self::select_all))
|
.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::home))
|
||||||
.on_action(cx.listener(Self::end))
|
.on_action(cx.listener(Self::end))
|
||||||
.on_action(cx.listener(Self::show_character_palette))
|
.on_action(cx.listener(Self::show_character_palette))
|
||||||
|
|
@ -795,6 +851,7 @@ impl Render for TextInput {
|
||||||
.items_center()
|
.items_center()
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
|
.id("TextElement")
|
||||||
.flex_grow()
|
.flex_grow()
|
||||||
.overflow_x_hidden()
|
.overflow_x_hidden()
|
||||||
.cursor_text()
|
.cursor_text()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue