Improve Input (#29)

Continue #27 

- Fix selection color in dark mode.
- Allow to continue selection, even mouse is out of the input.
- Pause cursor blinking when keep typing.

https://github.com/user-attachments/assets/ce9e722c-5c47-4daa-b2ba-cbe1680c4932
This commit is contained in:
Jason Lee 2024-07-13 00:34:55 +08:00 committed by GitHub
parent 0d6d1c95d0
commit 41a83e6f6f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 65 additions and 26 deletions

View file

@ -118,9 +118,12 @@ impl TextInput {
// Blink the cursor when the window is active, pause when it's not. // Blink the cursor when the window is active, pause when it's not.
cx.observe_window_activation(|input, cx| { cx.observe_window_activation(|input, cx| {
if cx.is_window_active() { if cx.is_window_active() {
input.blink_cursor.update(cx, |blink_cursor, cx| { let focus_handle = input.focus_handle.clone();
blink_cursor.start(cx); if focus_handle.is_focused(cx) {
}); input.blink_cursor.update(cx, |blink_cursor, cx| {
blink_cursor.start(cx);
});
}
} }
}) })
.detach(); .detach();
@ -230,7 +233,8 @@ impl TextInput {
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)
} }
self.replace_text_in_range(None, "", cx) self.replace_text_in_range(None, "", cx);
self.pause_blink_cursor(cx);
} }
fn delete(&mut self, _: &Delete, cx: &mut ViewContext<Self>) { fn delete(&mut self, _: &Delete, cx: &mut ViewContext<Self>) {
@ -267,7 +271,7 @@ impl TextInput {
self.replace_text_in_range(Some(self.selected_range.clone()), "", cx); self.replace_text_in_range(Some(self.selected_range.clone()), "", cx);
} }
pub 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(Some(self.selected_range.clone()), &new_text, cx);
@ -364,22 +368,32 @@ impl TextInput {
} }
fn on_focus(&mut self, cx: &mut ViewContext<Self>) { fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
self.blink_cursor.update(cx, |blink_cursor, cx| { self.blink_cursor.update(cx, |cursor, cx| {
blink_cursor.start(cx); cursor.start(cx);
}); });
} }
fn on_blur(&mut self, cx: &mut ViewContext<Self>) { fn on_blur(&mut self, cx: &mut ViewContext<Self>) {
self.unselect(cx); self.unselect(cx);
self.blink_cursor.update(cx, |blink_cursor, cx| { self.blink_cursor.update(cx, |cursor, cx| {
blink_cursor.pause(cx); cursor.stop(cx);
}); });
} }
fn pause_blink_cursor(&mut self, cx: &mut ViewContext<Self>) {
self.blink_cursor.update(cx, |cursor, cx| {
cursor.pause(cx);
});
}
fn on_key_down_for_blink_cursor(&mut self, _: &KeyDownEvent, cx: &mut ViewContext<Self>) {
self.pause_blink_cursor(cx)
}
fn on_mouse_left_down( fn on_mouse_left_down(
&mut self, &mut self,
event: &MouseDownEvent, event: &MouseDownEvent,
text_hitbox: Hitbox, hitbox: Hitbox,
cx: &mut ViewContext<TextInput>, cx: &mut ViewContext<TextInput>,
) { ) {
// Ignore if text is empty // Ignore if text is empty
@ -387,11 +401,11 @@ impl TextInput {
return; return;
} }
if !text_hitbox.contains(&event.position) { if !hitbox.contains(&event.position) {
return; return;
} }
let offset = self.offset_of_position(event.position, &text_hitbox); let offset = self.offset_of_position(event.position, &hitbox);
if event.modifiers.shift { if event.modifiers.shift {
self.select_to(offset, cx); self.select_to(offset, cx);
} else { } else {
@ -399,12 +413,7 @@ impl TextInput {
} }
} }
fn on_drag_move( fn on_drag_move(&mut self, event: &MouseMoveEvent, hitbox: Hitbox, cx: &mut ViewContext<Self>) {
&mut self,
event: &MouseMoveEvent,
text_hitbox: Hitbox,
cx: &mut ViewContext<Self>,
) {
// Ignore if text is empty // Ignore if text is empty
if self.text.is_empty() { if self.text.is_empty() {
return; return;
@ -414,11 +423,11 @@ impl TextInput {
return; return;
} }
if !text_hitbox.contains(&event.position) { if !self.focus_handle.is_focused(cx) {
return; return;
} }
let offset = self.offset_of_position(event.position, &text_hitbox); let offset = self.offset_of_position(event.position, &hitbox);
if offset == self.cursor_offset() { if offset == self.cursor_offset() {
return; return;
} }
@ -696,7 +705,7 @@ impl Element for TextElement {
point(bounds.left() + cursor_pos, bounds.top()), point(bounds.left() + cursor_pos, bounds.top()),
size(px(1.5), bounds.bottom() - bounds.top()), size(px(1.5), bounds.bottom() - bounds.top()),
), ),
gpui::blue(), crate::blue_500(),
)), )),
) )
} else { } else {
@ -789,6 +798,7 @@ impl Render for TextInput {
.on_double_click(cx.listener(|view, _, cx| { .on_double_click(cx.listener(|view, _, cx| {
view.select_all(&SelectAll, cx); view.select_all(&SelectAll, cx);
})) }))
.on_key_down(cx.listener(Self::on_key_down_for_blink_cursor))
.size_full() .size_full()
.line_height(rems(1.25)) .line_height(rems(1.25))
.text_size(rems(0.875)) .text_size(rems(0.875))

View file

@ -1,6 +1,8 @@
use std::time::Duration; use std::time::Duration;
use gpui::{ModelContext, Timer}; use gpui::{ModelContext, Timer, WeakView};
use super::TextInput;
/// To manage the Input cursor blinking. /// To manage the Input cursor blinking.
/// ///
@ -34,17 +36,21 @@ impl BlinkCursor {
} }
self.started = true; self.started = true;
self.paused = false;
self.blink(self.blink_epoch, cx); self.blink(self.blink_epoch, cx);
} }
pub fn stop(&mut self, cx: &mut ModelContext<Self>) {
self.started = false;
cx.notify();
}
fn next_epoch(&mut self) -> usize { fn next_epoch(&mut self) -> usize {
self.blink_epoch += 1; self.blink_epoch += 1;
self.blink_epoch self.blink_epoch
} }
fn blink(&mut self, epoch: usize, cx: &mut ModelContext<Self>) { fn blink(&mut self, epoch: usize, cx: &mut ModelContext<Self>) {
if self.paused { if self.paused || !self.started {
return; return;
} }
@ -69,12 +75,35 @@ impl BlinkCursor {
} }
pub fn visible(&self) -> bool { pub fn visible(&self) -> bool {
// Keep showing the cursor if paused
if self.paused {
return true;
}
self.visible self.visible
} }
/// Pause the blinking, and delay 500ms to resume the blinking.
pub fn pause(&mut self, cx: &mut ModelContext<Self>) { pub fn pause(&mut self, cx: &mut ModelContext<Self>) {
self.paused = true; self.paused = true;
self.started = false; self.next_epoch();
cx.notify(); cx.notify();
let epoch = self.next_epoch();
// delay 500ms to start the blinking
cx.spawn(|this, mut cx| async move {
Timer::after(Duration::from_secs_f64(0.5)).await;
if let Some(this) = this.upgrade() {
this.update(&mut cx, |this, cx| {
if epoch != this.blink_epoch {
return;
}
this.paused = false;
this.blink(epoch, cx);
})
.ok();
}
})
.detach();
} }
} }

View file

@ -250,7 +250,7 @@ impl Colors {
border: hsl(240.0, 3.7, 15.9), border: hsl(240.0, 3.7, 15.9),
input: hsl(240.0, 3.7, 15.9), input: hsl(240.0, 3.7, 15.9),
ring: hsl(240.0, 4.9, 83.9), ring: hsl(240.0, 4.9, 83.9),
selection: hsl(211.0, 97.0, 85.0), selection: hsl(211.0, 97.0, 22.0),
scrollbar: Hsla::transparent_black(), scrollbar: Hsla::transparent_black(),
scrollbar_thumb: hsl(240.0, 3.7, 15.9).opacity(0.7), scrollbar_thumb: hsl(240.0, 3.7, 15.9).opacity(0.7),
panel: hsl(299.0, 2., 9.), panel: hsl(299.0, 2., 9.),