Skip to content
Merged
Show file tree
Hide file tree
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
41 changes: 41 additions & 0 deletions crates/base/src/input/base/blink_cursor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,17 @@ impl BlinkCursor {
}

/// Show the cursor immediately and restart the idle delay before blinking resumes.
///
/// This is a no-op on a cursor that is not blinking. `epoch` is zero only
/// before the first [`Self::start`] and after [`Self::stop`], so a zero
/// epoch means the input is not focused, and there is no cursor on screen
/// to keep visible. Pausing it anyway would start a blink loop that no blur
/// is left to stop, and every blink repaints the view that the input is in.
pub(crate) fn pause(&mut self, cx: &mut Context<Self>) {
if self.epoch == 0 {
return;
}

self.paused = true;
self.visible = true;
cx.notify();
Expand Down Expand Up @@ -118,6 +128,9 @@ mod tests {
fn repeated_pauses_keep_cursor_visible_until_idle(cx: &mut TestAppContext) {
let cursor = cx.new(|_| BlinkCursor::new());
assert!(!cursor.read_with(cx, |cursor, _| cursor.visible()));
// Only a focused input blinks, and only a blinking cursor pauses.
cursor.update(cx, |cursor, cx| cursor.start(cx));
cx.run_until_parked();
for _ in 0..5 {
cursor.update(cx, |cursor, cx| cursor.pause(cx));
cx.run_until_parked();
Expand All @@ -133,6 +146,34 @@ mod tests {
assert!(cursor.read_with(cx, |cursor, _| cursor.visible()));
}

#[gpui::test]
fn pausing_a_cursor_that_is_not_blinking_does_not_start_it(cx: &mut TestAppContext) {
// Never focused, so `start` was never called: the epoch is still zero.
let cursor = cx.new(|_| BlinkCursor::new());

// What a programmatic `set_value` on an unfocused input does.
cursor.update(cx, |cursor, cx| cursor.pause(cx));
cx.run_until_parked();
cx.executor().advance_clock(PAUSE_DELAY);
cx.run_until_parked();
assert!(!cursor.read_with(cx, |cursor, _| cursor.visible()));

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

cx.executor().advance_clock(INTERVAL * 6);
cx.run_until_parked();

assert_eq!(
notifies.get(),
0,
"a cursor that was never started is blinking, and every blink repaints the view"
);
}

#[gpui::test]
fn blurring_a_paused_cursor_leaves_the_next_focus_blinking(cx: &mut TestAppContext) {
let cursor = cx.new(|_| BlinkCursor::new());
Expand Down
74 changes: 66 additions & 8 deletions crates/base/src/input/base/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4856,6 +4856,12 @@ mod tests {
cx.run_until_parked();
let input = input.unwrap();
let before = input.read_with(cx, |state, _| state.cursor_layout());
// The caret geometry only exists once the input has painted, and a
// notify sent from inside a draw marks the view dirty without asking
// for another frame, so the consumer reads the geometry on the next
// invalidation rather than during the paint that produced it.
input.update(cx, |_, cx| cx.notify());
cx.run_until_parked();
assert_eq!(observed.get(), before);
window
.update(cx, |_, _, cx| {
Expand Down Expand Up @@ -4935,6 +4941,46 @@ mod tests {
);
}

#[gpui::test]
fn test_set_value_on_unfocused_input_stays_quiet(cx: &mut TestAppContext) {
use std::{cell::Cell, rc::Rc};

cx.update(crate::init);
let mut input = None;
let window = cx.open_window(size(px(400.), px(100.)), |window, cx| {
input = Some(cx.new(|cx| crate::input::InputState::new(window, cx)));
gpui::EmptyView
});
let input = input.unwrap();
cx.run_until_parked();

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

// Seeding a form is a write, so it notifies once. The input is not
// focused and draws no caret, so nothing may notify after that.
window
.update(cx, |_, window, cx| {
input.update(cx, |state, cx| state.set_value("seeded", window, cx));
})
.unwrap();
cx.run_until_parked();
let settled = notifications.get();

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

assert_eq!(
notifications.get(),
settled,
"an unfocused input is blinking, and every blink repaints the view it is in"
);
}

#[gpui::test]
fn test_input_does_not_invalidate_cached_parent_during_paint(cx: &mut TestAppContext) {
use std::{cell::Cell, rc::Rc};
Expand Down Expand Up @@ -7421,13 +7467,25 @@ mod tests {
let view = InputView::<EditorMode>::new(cx);
let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
setup_cursors(&mut cx, &view.input, "ab\na|b\nab");
cx.update(|window, cx| {
view.input.update(cx, |state, cx| {
// Start each action in the hidden phase without depending on a
// key-down listener: actions and text input also arrive directly.
for action in 0..6 {
// Start each action in the hidden phase without depending on a
// key-down listener: actions and text input also arrive directly.
for action in 0..6 {
cx.update(|_, cx| {
view.input.update(cx, |state, cx| {
state.blink_cursor = cx.new(|_| BlinkCursor::new());
assert!(!state.blink_cursor.read(cx).visible());
state.blink_cursor.update(cx, |cursor, cx| cursor.start(cx));
});
});
cx.run_until_parked();
cx.executor()
.advance_clock(std::time::Duration::from_millis(500));
cx.run_until_parked();
view.input.read_with(&cx, |state, cx| {
assert!(!state.blink_cursor.read(cx).visible(), "action {action}");
});

cx.update(|window, cx| {
view.input.update(cx, |state, cx| {
match action {
0 => state.add_cursor_above(&AddCursorAbove, window, cx),
1 => state.add_cursor_below(&AddCursorBelow, window, cx),
Expand All @@ -7437,9 +7495,9 @@ mod tests {
_ => state.backspace(&Backspace, window, cx),
}
assert!(state.blink_cursor.read(cx).visible(), "action {action}");
}
});
});
});
}
}

#[gpui::test]
Expand Down
Loading