From 2785f38eebd99e3df740c688b9df521f69fbdf89 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Sat, 29 Jun 2024 15:38:02 +0800 Subject: [PATCH] Add to blinking currsor support for Input. --- crates/ui/src/input.rs | 50 ++++++++++++++++-- crates/ui/src/input/blink_cursor.rs | 80 +++++++++++++++++++++++++++++ 2 files changed, 126 insertions(+), 4 deletions(-) create mode 100644 crates/ui/src/input/blink_cursor.rs diff --git a/crates/ui/src/input.rs b/crates/ui/src/input.rs index cb152510..cd268c9e 100644 --- a/crates/ui/src/input.rs +++ b/crates/ui/src/input.rs @@ -2,10 +2,13 @@ use std::ops::Range; use crate::event::InterativeElementExt as _; use crate::theme::ActiveTheme; +use blink_cursor::BlinkCursor; use gpui::*; use prelude::FluentBuilder as _; use unicode_segmentation::*; +mod blink_cursor; + actions!( input, [ @@ -71,6 +74,7 @@ pub struct TextInput { focus_handle: FocusHandle, text: SharedString, placeholder: SharedString, + blink_cursor: Model, selected_range: Range, selection_reversed: bool, marked_range: Option>, @@ -84,10 +88,13 @@ impl EventEmitter for TextInput {} impl TextInput { pub fn new(cx: &mut ViewContext) -> Self { - Self { - focus_handle: cx.focus_handle(), + let focus_handle = cx.focus_handle(); + let blink_cursor = cx.new_model(|cx| BlinkCursor::new(cx)); + let input = Self { + focus_handle: focus_handle.clone(), text: "".into(), placeholder: "".into(), + blink_cursor, selected_range: 0..0, selection_reversed: false, marked_range: None, @@ -95,7 +102,25 @@ impl TextInput { disabled: false, masked: false, appearance: true, - } + }; + + // Observe the blink cursor to repaint the view when it changes. + cx.observe(&input.blink_cursor, |_, _, cx| cx.notify()) + .detach(); + // Blink the cursor when the window is active, pause when it's not. + cx.observe_window_activation(|input, cx| { + if cx.is_window_active() { + input.blink_cursor.update(cx, |blink_cursor, cx| { + blink_cursor.start(cx); + }); + } + }) + .detach(); + + cx.on_focus(&focus_handle, Self::on_focus).detach(); + cx.on_blur(&focus_handle, Self::on_blur).detach(); + + input } /// Set the text of the input field. @@ -288,6 +313,23 @@ impl TextInput { .find_map(|(idx, _)| (idx > offset).then_some(idx)) .unwrap_or(self.text.len()) } + + /// Returns the true to let InputElement to render cursor, when Input is focused and current BlinkCursor is visible. + pub(crate) fn show_cursor(&self, cx: &WindowContext) -> bool { + self.focus_handle.is_focused(cx) && self.blink_cursor.read(cx).visible() + } + + fn on_focus(&mut self, cx: &mut ViewContext) { + self.blink_cursor.update(cx, |blink_cursor, cx| { + blink_cursor.start(cx); + }); + } + + fn on_blur(&mut self, cx: &mut ViewContext) { + self.blink_cursor.update(cx, |blink_cursor, cx| { + blink_cursor.pause(cx); + }); + } } impl ViewInputHandler for TextInput { @@ -496,7 +538,7 @@ impl Element for TextElement { .unwrap(); let cursor_pos = line.x_for_index(cursor); - let (selection, cursor) = if selected_range.is_empty() { + let (selection, cursor) = if selected_range.is_empty() && input.show_cursor(cx) { ( None, Some(fill( diff --git a/crates/ui/src/input/blink_cursor.rs b/crates/ui/src/input/blink_cursor.rs new file mode 100644 index 00000000..0c9cd54f --- /dev/null +++ b/crates/ui/src/input/blink_cursor.rs @@ -0,0 +1,80 @@ +use std::time::Duration; + +use gpui::{ModelContext, Timer}; + +/// To manage the Input cursor blinking. +/// +/// It will start blinking with a interval of 500ms. +/// Every loop will notify the view to update the `visable`, and Input will observe this update to touch repaint. +/// +/// The input painter will check if this in visible state, then it will draw the cursor. +pub struct BlinkCursor { + interval: Duration, + blink_epoch: usize, + visible: bool, + paused: bool, + started: bool, +} + +impl BlinkCursor { + pub fn new(_cx: &mut ModelContext) -> Self { + Self { + interval: Duration::from_millis(500), + visible: false, + paused: false, + started: false, + blink_epoch: 0, + } + } + + /// Start the blinking + pub fn start(&mut self, cx: &mut ModelContext) { + if self.started { + return; + } + + self.started = true; + self.paused = false; + self.blink(self.blink_epoch, cx); + } + + fn next_epoch(&mut self) -> usize { + self.blink_epoch += 1; + self.blink_epoch + } + + fn blink(&mut self, epoch: usize, cx: &mut ModelContext) { + if self.paused { + return; + } + + if epoch != self.blink_epoch { + return; + } + + self.visible = !self.visible; + cx.notify(); + + let epoch = self.next_epoch(); + + // Schedule the next blink + let interval = self.interval; + cx.spawn(|this, mut cx| async move { + Timer::after(interval).await; + if let Some(this) = this.upgrade() { + this.update(&mut cx, |this, cx| this.blink(epoch, cx)).ok(); + } + }) + .detach(); + } + + pub fn visible(&self) -> bool { + self.visible + } + + pub fn pause(&mut self, cx: &mut ModelContext) { + self.paused = true; + self.started = false; + cx.notify(); + } +}