1use gpui::{App, Context, Pixels, Subscription, Task, px};
2use instant::Duration;
3use web_time::Instant;
4
5static INTERVAL: Duration = Duration::from_millis(500);
6static PAUSE_DELAY: Duration = Duration::from_millis(300);
7
8pub(super) const CURSOR_WIDTH: Pixels = px(1.);
9
10#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14pub struct CursorBlink {
15 interval: Duration,
16 idle_timeout: Option<Duration>,
17}
18
19impl CursorBlink {
20 pub const fn steady() -> Self {
22 Self {
23 interval: Duration::ZERO,
24 idle_timeout: None,
25 }
26 }
27
28 pub const fn blinking(interval: Duration) -> Self {
30 Self {
31 interval,
32 idle_timeout: None,
33 }
34 }
35
36 pub const fn with_idle_timeout(mut self, timeout: Duration) -> Self {
39 self.idle_timeout = Some(timeout);
40 self
41 }
42
43 pub fn interval(&self) -> Option<Duration> {
45 (!self.interval.is_zero()).then_some(self.interval)
46 }
47
48 pub fn idle_timeout(&self) -> Option<Duration> {
50 self.interval().and(self.idle_timeout)
51 }
52
53 pub fn is_steady(&self) -> bool {
55 self.interval.is_zero()
56 }
57
58 fn normalized(self) -> Self {
61 if self.is_steady() {
62 Self::steady()
63 } else {
64 self
65 }
66 }
67}
68
69impl Default for CursorBlink {
70 fn default() -> Self {
71 Self::blinking(INTERVAL)
72 }
73}
74
75fn cursor_blink(cx: &App) -> CursorBlink {
76 cx.try_global::<crate::Theme>()
77 .map(|theme| theme.cursor_blink)
78 .unwrap_or_default()
79 .normalized()
80}
81
82pub(crate) struct BlinkCursor {
83 visible: bool,
84 paused: bool,
85 epoch: usize,
86 last_activity: Option<Instant>,
87 policy: CursorBlink,
88 _task: Task<()>,
89 _policy_subscription: Subscription,
90}
91
92impl BlinkCursor {
93 pub(crate) fn new(cx: &mut Context<Self>) -> Self {
94 Self {
95 visible: false,
96 paused: false,
97 epoch: 0,
98 last_activity: None,
99 policy: cursor_blink(cx),
100 _task: Task::ready(()),
101 _policy_subscription: cx.observe_global::<crate::Theme>(|this, cx| {
102 let policy = cursor_blink(cx);
103 if policy == this.policy {
104 return;
105 }
106 this.policy = policy;
107 if this.last_activity.is_some() {
108 this.epoch = this.epoch.wrapping_add(1);
109 this.set_visible(true, cx);
110 this.schedule(cx);
111 }
112 }),
113 }
114 }
115
116 pub(crate) fn start(&mut self, cx: &mut Context<Self>) {
117 self.epoch = self.epoch.wrapping_add(1);
118 self.last_activity = Some(cx.background_executor().now());
119 self.paused = false;
120 self.set_visible(true, cx);
121 self.schedule(cx);
122 }
123
124 pub(crate) fn stop(&mut self, cx: &mut Context<Self>) {
125 self.epoch = self.epoch.wrapping_add(1);
126 self.last_activity = None;
127 self.paused = false;
128 self._task = Task::ready(());
129 self.set_visible(false, cx);
130 }
131
132 pub(crate) fn pause(&mut self, cx: &mut Context<Self>) {
133 if self.last_activity.is_none() {
134 return;
135 }
136
137 self.epoch = self.epoch.wrapping_add(1);
138 self.last_activity = Some(cx.background_executor().now());
139 self.paused = true;
140 self.set_visible(true, cx);
141 self.schedule(cx);
142 }
143
144 pub(crate) fn visible(&self) -> bool {
145 self.visible
146 }
147
148 fn set_visible(&mut self, visible: bool, cx: &mut Context<Self>) {
149 if self.visible != visible {
150 self.visible = visible;
151 cx.notify();
152 }
153 }
154
155 fn blink_delay(&self, cx: &App) -> Option<Duration> {
156 let last_activity = self.last_activity?;
157 let interval = self.policy.interval()?;
158
159 let elapsed = cx.background_executor().now() - last_activity;
160 let delay = if self.paused && elapsed < PAUSE_DELAY {
161 PAUSE_DELAY - elapsed
162 } else {
163 interval
164 };
165
166 match self.policy.idle_timeout() {
167 Some(timeout) if elapsed >= timeout => None,
168 Some(timeout) => Some(delay.min(timeout - elapsed)),
169 None => Some(delay),
170 }
171 }
172
173 fn schedule(&mut self, cx: &mut Context<Self>) {
174 self._task = Task::ready(());
175 let Some(delay) = self.blink_delay(cx) else {
176 self.paused = false;
177 self.set_visible(true, cx);
178 return;
179 };
180
181 let epoch = self.epoch;
182 let timer = cx.background_executor().timer(delay);
183 self._task = cx.spawn(async move |this, cx| {
184 timer.await;
185 _ = this.update(cx, |this, cx| {
186 if epoch != this.epoch {
187 return;
188 }
189
190 if this.blink_delay(cx).is_some() {
191 this.paused = false;
192 this.set_visible(!this.visible, cx);
193 }
194 this.schedule(cx);
195 });
196 });
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203 use gpui::{AppContext as _, Entity, TestAppContext};
204 use std::{cell::Cell, rc::Rc};
205
206 fn set_cursor_blink(policy: CursorBlink, cx: &mut App) {
207 crate::Theme::global_mut(cx).cursor_blink = policy;
208 }
209
210 #[test]
211 fn test_cursor_blink_builder() {
212 let policy = CursorBlink::blinking(INTERVAL).with_idle_timeout(Duration::from_secs(5));
213 assert_eq!(policy.interval(), Some(INTERVAL));
214 assert_eq!(policy.idle_timeout(), Some(Duration::from_secs(5)));
215 assert!(!policy.is_steady());
216 assert_eq!(CursorBlink::default(), CursorBlink::blinking(INTERVAL));
217
218 let steady = CursorBlink::blinking(Duration::ZERO).with_idle_timeout(INTERVAL);
219 assert!(steady.is_steady());
220 assert_eq!(steady.interval(), None);
221 assert_eq!(steady.idle_timeout(), None);
222 assert_eq!(steady.normalized(), CursorBlink::steady());
223 }
224
225 fn advance(cx: &mut TestAppContext, duration: Duration) {
226 cx.run_until_parked();
227 cx.executor().advance_clock(duration);
228 cx.run_until_parked();
229 }
230
231 fn assert_idle(cursor: &Entity<BlinkCursor>, visible: bool, cx: &mut TestAppContext) {
232 cx.run_until_parked();
233 let notifications = Rc::new(Cell::new(0usize));
234 let count = notifications.clone();
235 let _observer = cx.update(|cx| cx.observe(cursor, move |_, _| count.set(count.get() + 1)));
236
237 advance(cx, Duration::from_secs(3));
238
239 assert_eq!(cursor.read_with(cx, |cursor, _| cursor.visible()), visible);
240 assert!(cursor.read_with(cx, |cursor, _| cursor._task.is_ready()));
241 assert_eq!(notifications.get(), 0);
242 }
243
244 #[gpui::test]
245 fn repeated_pauses_keep_cursor_visible_until_idle(cx: &mut TestAppContext) {
246 let cursor = cx.new(BlinkCursor::new);
247 assert!(!cursor.read_with(cx, |cursor, _| cursor.visible()));
248 cursor.update(cx, |cursor, cx| cursor.start(cx));
250 cx.run_until_parked();
251 for _ in 0..5 {
252 cursor.update(cx, |cursor, cx| cursor.pause(cx));
253 cx.run_until_parked();
254 cx.executor().advance_clock(Duration::from_millis(200));
255 cx.run_until_parked();
256 assert!(cursor.read_with(cx, |cursor, _| cursor.visible()));
257 }
258 cx.executor().advance_clock(Duration::from_millis(100));
259 cx.run_until_parked();
260 assert!(!cursor.read_with(cx, |cursor, _| cursor.visible()));
261 cx.executor().advance_clock(INTERVAL);
262 cx.run_until_parked();
263 assert!(cursor.read_with(cx, |cursor, _| cursor.visible()));
264 }
265
266 #[gpui::test]
267 fn starting_a_blinking_cursor_keeps_it_visible(cx: &mut TestAppContext) {
268 let cursor = cx.new(BlinkCursor::new);
269
270 cursor.update(cx, |cursor, cx| cursor.start(cx));
273 cursor.update(cx, |cursor, cx| cursor.start(cx));
274 cx.run_until_parked();
275 assert!(cursor.read_with(cx, |cursor, _| cursor.visible()));
276
277 cx.executor()
280 .advance_clock(INTERVAL - Duration::from_millis(1));
281 cx.run_until_parked();
282 assert!(cursor.read_with(cx, |cursor, _| cursor.visible()));
283 cx.executor().advance_clock(Duration::from_millis(1));
284 cx.run_until_parked();
285 assert!(!cursor.read_with(cx, |cursor, _| cursor.visible()));
286 }
287
288 #[gpui::test]
289 fn pausing_a_cursor_that_is_not_blinking_does_not_start_it(cx: &mut TestAppContext) {
290 let cursor = cx.new(BlinkCursor::new);
292
293 cursor.update(cx, |cursor, cx| cursor.pause(cx));
295 cx.run_until_parked();
296 cx.executor().advance_clock(PAUSE_DELAY);
297 cx.run_until_parked();
298 assert!(!cursor.read_with(cx, |cursor, _| cursor.visible()));
299
300 let notifies = Rc::new(Cell::new(0usize));
301 let counter = notifies.clone();
302 let _observer =
303 cx.update(|cx| cx.observe(&cursor, move |_, _| counter.set(counter.get() + 1)));
304 cx.run_until_parked();
305
306 cx.executor().advance_clock(INTERVAL * 6);
307 cx.run_until_parked();
308
309 assert_eq!(
310 notifies.get(),
311 0,
312 "a cursor that was never started is blinking, and every blink repaints the view"
313 );
314 }
315
316 #[gpui::test]
317 fn blurring_a_paused_cursor_leaves_the_next_focus_blinking(cx: &mut TestAppContext) {
318 let cursor = cx.new(BlinkCursor::new);
319 cursor.update(cx, |cursor, cx| cursor.start(cx));
320 cx.run_until_parked();
321
322 cursor.update(cx, |cursor, cx| cursor.pause(cx));
325 cx.run_until_parked();
326 cursor.update(cx, |cursor, cx| cursor.stop(cx));
327 cx.run_until_parked();
328 assert!(!cursor.read_with(cx, |cursor, _| cursor.visible()));
329
330 cursor.update(cx, |cursor, cx| cursor.start(cx));
333 cx.run_until_parked();
334 assert!(cursor.read_with(cx, |cursor, _| cursor.visible()));
335 cx.executor().advance_clock(INTERVAL);
336 cx.run_until_parked();
337 assert!(!cursor.read_with(cx, |cursor, _| cursor.visible()));
338 }
339
340 #[gpui::test]
341 fn stopping_a_paused_cursor_ends_the_blink_loop(cx: &mut TestAppContext) {
342 let cursor = cx.new(BlinkCursor::new);
343 cursor.update(cx, |cursor, cx| cursor.start(cx));
344 cx.run_until_parked();
345 cursor.update(cx, |cursor, cx| cursor.pause(cx));
346 cx.run_until_parked();
347 cursor.update(cx, |cursor, cx| cursor.stop(cx));
348 cx.run_until_parked();
349
350 let notifications = Rc::new(Cell::new(0usize));
351 let count = notifications.clone();
352 let _observer = cx.update(|cx| cx.observe(&cursor, move |_, _| count.set(count.get() + 1)));
353 cx.run_until_parked();
354
355 cx.executor().advance_clock(Duration::from_secs(3));
356 cx.run_until_parked();
357
358 assert!(!cursor.read_with(cx, |cursor, _| cursor.visible()));
359 assert_eq!(notifications.get(), 0, "a stopped cursor kept blinking");
360
361 cursor.update(cx, |cursor, cx| cursor.start(cx));
362 cx.run_until_parked();
363 assert!(cursor.read_with(cx, |cursor, _| cursor.visible()));
364 }
365
366 #[gpui::test]
367 fn stopping_a_blinking_cursor_ends_the_blink_loop(cx: &mut TestAppContext) {
368 let cursor = cx.new(BlinkCursor::new);
369 cursor.update(cx, |cursor, cx| cursor.start(cx));
370 cx.run_until_parked();
371 cursor.update(cx, |cursor, cx| cursor.stop(cx));
372 cx.run_until_parked();
373
374 let notifications = Rc::new(Cell::new(0usize));
375 let count = notifications.clone();
376 let _observer = cx.update(|cx| cx.observe(&cursor, move |_, _| count.set(count.get() + 1)));
377 cx.run_until_parked();
378
379 cx.executor().advance_clock(Duration::from_secs(3));
380 cx.run_until_parked();
381
382 assert!(!cursor.read_with(cx, |cursor, _| cursor.visible()));
383 assert_eq!(notifications.get(), 0, "a stopped cursor kept blinking");
384 }
385
386 #[gpui::test]
387 fn idle_timeout_stops_between_blinks_and_activity_restarts_it(cx: &mut TestAppContext) {
388 cx.update(|cx| {
389 set_cursor_blink(
390 CursorBlink::blinking(INTERVAL).with_idle_timeout(Duration::from_millis(750)),
391 cx,
392 )
393 });
394 let cursor = cx.new(BlinkCursor::new);
395 cursor.update(cx, |cursor, cx| cursor.start(cx));
396
397 advance(cx, INTERVAL);
398 assert!(!cursor.read_with(cx, |cursor, _| cursor.visible()));
399 advance(cx, Duration::from_millis(249));
400 assert!(!cursor.read_with(cx, |cursor, _| cursor.visible()));
401 advance(cx, Duration::from_millis(1));
402 assert_idle(&cursor, true, cx);
403
404 cursor.update(cx, |cursor, cx| cursor.pause(cx));
405 advance(cx, PAUSE_DELAY);
406 assert!(!cursor.read_with(cx, |cursor, _| cursor.visible()));
407 advance(cx, Duration::from_millis(450));
408 assert_idle(&cursor, true, cx);
409
410 cursor.update(cx, |cursor, cx| cursor.start(cx));
411 advance(cx, INTERVAL);
412 assert!(!cursor.read_with(cx, |cursor, _| cursor.visible()));
413 }
414
415 #[gpui::test]
416 fn idle_timeout_includes_the_typing_pause(cx: &mut TestAppContext) {
417 cx.update(|cx| {
418 set_cursor_blink(
419 CursorBlink::blinking(INTERVAL).with_idle_timeout(Duration::from_millis(200)),
420 cx,
421 )
422 });
423 let cursor = cx.new(BlinkCursor::new);
424 cursor.update(cx, |cursor, cx| cursor.start(cx));
425 advance(cx, Duration::from_millis(100));
426 cursor.update(cx, |cursor, cx| cursor.pause(cx));
427 advance(cx, Duration::from_millis(199));
428 assert!(cursor.read_with(cx, |cursor, _| cursor.visible()));
429 assert!(!cursor.read_with(cx, |cursor, _| cursor._task.is_ready()));
430 advance(cx, Duration::from_millis(1));
431 assert_idle(&cursor, true, cx);
432 }
433
434 #[gpui::test]
435 fn steady_and_zero_durations_never_schedule_blinks(cx: &mut TestAppContext) {
436 for policy in [
437 CursorBlink::steady(),
438 CursorBlink::blinking(Duration::ZERO),
439 CursorBlink::blinking(INTERVAL).with_idle_timeout(Duration::ZERO),
440 ] {
441 cx.update(|cx| set_cursor_blink(policy, cx));
442 let cursor = cx.new(BlinkCursor::new);
443 cursor.update(cx, |cursor, cx| cursor.start(cx));
444 assert_idle(&cursor, true, cx);
445 cursor.update(cx, |cursor, cx| cursor.pause(cx));
446 assert_idle(&cursor, true, cx);
447 }
448 }
449
450 #[gpui::test]
451 fn policy_changes_resume_idle_cursors_without_resetting_activity(cx: &mut TestAppContext) {
452 let cursor = cx.new(BlinkCursor::new);
453 cursor.update(cx, |cursor, cx| cursor.start(cx));
454 advance(cx, INTERVAL);
455 assert!(!cursor.read_with(cx, |cursor, _| cursor.visible()));
456
457 cx.update(|cx| set_cursor_blink(CursorBlink::steady(), cx));
458 assert!(cursor.read_with(cx, |cursor, _| cursor.visible()));
459 advance(cx, Duration::from_secs(1));
460
461 cx.update(|cx| {
462 set_cursor_blink(
463 CursorBlink::blinking(Duration::from_millis(100))
464 .with_idle_timeout(Duration::from_secs(1)),
465 cx,
466 )
467 });
468 assert!(cursor.read_with(cx, |cursor, _| cursor._task.is_ready()));
469
470 cx.update(|cx| {
471 set_cursor_blink(
472 CursorBlink::blinking(Duration::from_millis(100))
473 .with_idle_timeout(Duration::from_secs(2)),
474 cx,
475 )
476 });
477 advance(cx, Duration::from_millis(100));
478 assert!(!cursor.read_with(cx, |cursor, _| cursor.visible()));
479 advance(cx, Duration::from_millis(400));
480 assert!(cursor.read_with(cx, |cursor, _| cursor.visible()));
481 assert!(cursor.read_with(cx, |cursor, _| cursor._task.is_ready()));
482
483 cx.update(|cx| set_cursor_blink(CursorBlink::blinking(Duration::from_millis(100)), cx));
484 advance(cx, Duration::from_millis(100));
485 assert!(!cursor.read_with(cx, |cursor, _| cursor.visible()));
486 }
487
488 #[gpui::test]
489 fn changing_the_interval_restarts_the_visible_phase_and_cancels_old_blinks(
490 cx: &mut TestAppContext,
491 ) {
492 let cursor = cx.new(BlinkCursor::new);
493 cursor.update(cx, |cursor, cx| cursor.start(cx));
494 advance(cx, Duration::from_millis(250));
495
496 cx.update(|cx| set_cursor_blink(CursorBlink::blinking(Duration::from_secs(1)), cx));
497 advance(cx, Duration::from_millis(250));
498 assert!(cursor.read_with(cx, |cursor, _| cursor.visible()));
499 advance(cx, Duration::from_millis(749));
500 assert!(cursor.read_with(cx, |cursor, _| cursor.visible()));
501 advance(cx, Duration::from_millis(1));
502 assert!(!cursor.read_with(cx, |cursor, _| cursor.visible()));
503
504 cx.update(|cx| set_cursor_blink(CursorBlink::steady(), cx));
505 assert_idle(&cursor, true, cx);
506 }
507
508 #[gpui::test]
509 fn unchanged_policy_preserves_the_phase_and_idle_deadline(cx: &mut TestAppContext) {
510 let policy = CursorBlink::blinking(INTERVAL).with_idle_timeout(Duration::from_millis(750));
511 cx.update(|cx| set_cursor_blink(policy, cx));
512 let cursor = cx.new(BlinkCursor::new);
513 cursor.update(cx, |cursor, cx| cursor.start(cx));
514 advance(cx, INTERVAL);
515 assert!(!cursor.read_with(cx, |cursor, _| cursor.visible()));
516
517 cx.update(|cx| set_cursor_blink(policy, cx));
518 assert!(!cursor.read_with(cx, |cursor, _| cursor.visible()));
519 advance(cx, Duration::from_millis(249));
520 assert!(!cursor.read_with(cx, |cursor, _| cursor.visible()));
521 advance(cx, Duration::from_millis(1));
522 assert_idle(&cursor, true, cx);
523 }
524
525 #[gpui::test]
526 fn unrelated_theme_changes_preserve_the_blink_phase_and_idle_deadline(cx: &mut TestAppContext) {
527 cx.update(|cx| {
528 set_cursor_blink(
529 CursorBlink::blinking(INTERVAL).with_idle_timeout(Duration::from_millis(750)),
530 cx,
531 );
532 });
533 let cursor = cx.new(BlinkCursor::new);
534 cursor.update(cx, |cursor, cx| cursor.start(cx));
535 advance(cx, INTERVAL);
536 assert!(!cursor.read_with(cx, |cursor, _| cursor.visible()));
537
538 cx.update(|cx| crate::Theme::global_mut(cx).appearance = crate::ThemeAppearance::Dark);
539 assert!(!cursor.read_with(cx, |cursor, _| cursor.visible()));
540 advance(cx, Duration::from_millis(249));
541 assert!(!cursor.read_with(cx, |cursor, _| cursor.visible()));
542 advance(cx, Duration::from_millis(1));
543 assert_idle(&cursor, true, cx);
544 }
545
546 #[gpui::test]
547 fn policy_changes_preserve_the_remaining_typing_pause(cx: &mut TestAppContext) {
548 let cursor = cx.new(BlinkCursor::new);
549 cursor.update(cx, |cursor, cx| cursor.start(cx));
550 cursor.update(cx, |cursor, cx| cursor.pause(cx));
551 advance(cx, Duration::from_millis(100));
552
553 cx.update(|cx| {
554 set_cursor_blink(
555 CursorBlink::blinking(Duration::from_millis(200))
556 .with_idle_timeout(Duration::from_millis(400)),
557 cx,
558 )
559 });
560 advance(cx, Duration::from_millis(199));
561 assert!(cursor.read_with(cx, |cursor, _| cursor.visible()));
562 advance(cx, Duration::from_millis(1));
563 assert!(!cursor.read_with(cx, |cursor, _| cursor.visible()));
564 advance(cx, Duration::from_millis(100));
565 assert_idle(&cursor, true, cx);
566 }
567
568 #[gpui::test]
569 fn steady_policy_cancels_the_typing_pause(cx: &mut TestAppContext) {
570 let cursor = cx.new(BlinkCursor::new);
571 cursor.update(cx, |cursor, cx| cursor.start(cx));
572 cursor.update(cx, |cursor, cx| cursor.pause(cx));
573 advance(cx, Duration::from_millis(100));
574 cx.update(|cx| set_cursor_blink(CursorBlink::steady(), cx));
575 assert_idle(&cursor, true, cx);
576 }
577
578 #[gpui::test]
579 fn unfocused_cursors_ignore_policy_changes(cx: &mut TestAppContext) {
580 let never_focused = cx.new(BlinkCursor::new);
581 let blurred = cx.new(BlinkCursor::new);
582 blurred.update(cx, |cursor, cx| cursor.start(cx));
583 blurred.update(cx, |cursor, cx| cursor.pause(cx));
584 blurred.update(cx, |cursor, cx| cursor.stop(cx));
585
586 for policy in [CursorBlink::steady(), CursorBlink::default()] {
587 cx.update(|cx| set_cursor_blink(policy, cx));
588 assert_idle(&never_focused, false, cx);
589 assert_idle(&blurred, false, cx);
590 }
591
592 blurred.update(cx, |cursor, cx| cursor.start(cx));
593 advance(cx, INTERVAL);
594 assert!(!blurred.read_with(cx, |cursor, _| cursor.visible()));
595 }
596
597 #[gpui::test]
598 fn shortening_the_timeout_stops_a_hidden_cursor_immediately(cx: &mut TestAppContext) {
599 let cursor = cx.new(BlinkCursor::new);
600 cursor.update(cx, |cursor, cx| cursor.start(cx));
601 advance(cx, INTERVAL);
602 assert!(!cursor.read_with(cx, |cursor, _| cursor.visible()));
603
604 cx.update(|cx| {
605 set_cursor_blink(
606 CursorBlink::blinking(INTERVAL).with_idle_timeout(Duration::from_millis(400)),
607 cx,
608 )
609 });
610 assert_idle(&cursor, true, cx);
611 }
612
613 #[gpui::test]
614 fn initialization_preserves_an_application_policy(cx: &mut TestAppContext) {
615 cx.update(|cx| set_cursor_blink(CursorBlink::steady(), cx));
616 cx.update(crate::init);
617 let cursor = cx.new(BlinkCursor::new);
618 cursor.update(cx, |cursor, cx| cursor.start(cx));
619 assert_idle(&cursor, true, cx);
620 }
621}