Skip to main content

gpui_base/input/base/
blink_cursor.rs

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/// Application-wide caret visibility policy.
11///
12/// The default blinks every 500 ms without an idle timeout.
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14pub struct CursorBlink {
15    interval: Duration,
16    idle_timeout: Option<Duration>,
17}
18
19impl CursorBlink {
20    /// Keep the focused caret visible without a timer.
21    pub const fn steady() -> Self {
22        Self {
23            interval: Duration::ZERO,
24            idle_timeout: None,
25        }
26    }
27
28    /// Alternate visible and hidden phases of `interval` each; zero means steady.
29    pub const fn blinking(interval: Duration) -> Self {
30        Self {
31            interval,
32            idle_timeout: None,
33        }
34    }
35
36    /// Stop blinking, with the caret visible, once `timeout` has passed since
37    /// the last focus, edit or cursor movement.
38    pub const fn with_idle_timeout(mut self, timeout: Duration) -> Self {
39        self.idle_timeout = Some(timeout);
40        self
41    }
42
43    /// The duration of each visible or hidden phase, or `None` when steady.
44    pub fn interval(&self) -> Option<Duration> {
45        (!self.interval.is_zero()).then_some(self.interval)
46    }
47
48    /// The time since the last activity after which blinking stops.
49    pub fn idle_timeout(&self) -> Option<Duration> {
50        self.interval().and(self.idle_timeout)
51    }
52
53    /// Whether the focused caret stays visible without blinking.
54    pub fn is_steady(&self) -> bool {
55        self.interval.is_zero()
56    }
57
58    /// Collapse every steady spelling to one value, so a change between them
59    /// is not mistaken for a policy change.
60    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        // Only a focused input blinks, and only a blinking cursor pauses.
249        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        // Clicking into an input starts the cursor from `focus` and again from
271        // the focus observer; the second start must not hide the new cursor.
272        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        // The restart drops the first cycle's timer, so the cursor stays
278        // visible for a full interval before it blinks off.
279        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        // Never focused, so `start` was never called: the epoch is still zero.
291        let cursor = cx.new(BlinkCursor::new);
292
293        // What a programmatic `set_value` on an unfocused input does.
294        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        // Typing pauses the blink, then the input is blurred before the pause
323        // elapses: tabbing away right after a keystroke does exactly this.
324        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        // Focusing again shows the cursor and blinks it, rather than leaving a
331        // stale pause to swallow the start.
332        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}