Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 86 additions & 1 deletion crates/base/src/input/base/blink_cursor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,13 @@ impl BlinkCursor {
self.blink(self.epoch, cx);
}

/// Stop the blinking and clear the blink state, so the next [`Self::start`]
/// begins from a visible cursor instead of resuming a stale pause.
pub(crate) fn stop(&mut self, cx: &mut Context<Self>) {
self.epoch = 0;
self.paused = false;
self.visible = false;
self._task = Task::ready(());
cx.notify();
}

Expand All @@ -50,7 +55,12 @@ impl BlinkCursor {
}

fn blink(&mut self, epoch: usize, cx: &mut Context<Self>) {
if self.paused || epoch != self.epoch {
// A task from an earlier blink lifecycle must not mutate the current state.
if epoch != self.epoch {
return;
}

if self.paused {
self.visible = true;
return;
}
Expand Down Expand Up @@ -86,6 +96,10 @@ impl BlinkCursor {

if let Some(this) = this.upgrade() {
this.update(cx, |this, cx| {
if epoch != this.epoch {
return;
}

this.paused = false;
this.blink(epoch, cx);
});
Expand All @@ -98,6 +112,7 @@ impl BlinkCursor {
mod tests {
use super::*;
use gpui::{AppContext as _, TestAppContext};
use std::{cell::Cell, rc::Rc};

#[gpui::test]
fn repeated_pauses_keep_cursor_visible_until_idle(cx: &mut TestAppContext) {
Expand All @@ -117,4 +132,74 @@ mod tests {
cx.run_until_parked();
assert!(cursor.read_with(cx, |cursor, _| cursor.visible()));
}

#[gpui::test]
fn blurring_a_paused_cursor_leaves_the_next_focus_blinking(cx: &mut TestAppContext) {
let cursor = cx.new(|_| BlinkCursor::new());
cursor.update(cx, |cursor, cx| cursor.start(cx));
cx.run_until_parked();

// Typing pauses the blink, then the input is blurred before the pause
// elapses: tabbing away right after a keystroke does exactly this.
cursor.update(cx, |cursor, cx| cursor.pause(cx));
cx.run_until_parked();
cursor.update(cx, |cursor, cx| cursor.stop(cx));
cx.run_until_parked();
assert!(!cursor.read_with(cx, |cursor, _| cursor.visible()));

// Focusing again shows the cursor and blinks it, rather than leaving a
// stale pause to swallow the start.
cursor.update(cx, |cursor, cx| cursor.start(cx));
cx.run_until_parked();
assert!(cursor.read_with(cx, |cursor, _| cursor.visible()));
cx.executor().advance_clock(INTERVAL);
cx.run_until_parked();
assert!(!cursor.read_with(cx, |cursor, _| cursor.visible()));
}

#[gpui::test]
fn stopping_a_paused_cursor_ends_the_blink_loop(cx: &mut TestAppContext) {
let cursor = cx.new(|_| BlinkCursor::new());
cursor.update(cx, |cursor, cx| cursor.start(cx));
cx.run_until_parked();
cursor.update(cx, |cursor, cx| cursor.pause(cx));
cx.run_until_parked();
cursor.update(cx, |cursor, cx| cursor.stop(cx));
cx.run_until_parked();

let notifications = Rc::new(Cell::new(0usize));
let count = notifications.clone();
let _observer = cx.update(|cx| cx.observe(&cursor, move |_, _| count.set(count.get() + 1)));
cx.run_until_parked();

cx.executor().advance_clock(Duration::from_secs(3));
cx.run_until_parked();

assert!(!cursor.read_with(cx, |cursor, _| cursor.visible()));
assert_eq!(notifications.get(), 0, "a stopped cursor kept blinking");

cursor.update(cx, |cursor, cx| cursor.start(cx));
cx.run_until_parked();
assert!(cursor.read_with(cx, |cursor, _| cursor.visible()));
}

#[gpui::test]
fn stopping_a_blinking_cursor_ends_the_blink_loop(cx: &mut TestAppContext) {
let cursor = cx.new(|_| BlinkCursor::new());
cursor.update(cx, |cursor, cx| cursor.start(cx));
cx.run_until_parked();
cursor.update(cx, |cursor, cx| cursor.stop(cx));
cx.run_until_parked();

let notifications = Rc::new(Cell::new(0usize));
let count = notifications.clone();
let _observer = cx.update(|cx| cx.observe(&cursor, move |_, _| count.set(count.get() + 1)));
cx.run_until_parked();

cx.executor().advance_clock(Duration::from_secs(3));
cx.run_until_parked();

assert!(!cursor.read_with(cx, |cursor, _| cursor.visible()));
assert_eq!(notifications.get(), 0, "a stopped cursor kept blinking");
}
}
Loading