1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
//! Deciding when a loading indicator shows, so quick work never blinks one.
use std::time::Duration;
/// How long work runs before its indicator shows.
///
/// Around 300 ms is where people start to notice waiting; anything quicker feels instant, and an
/// indicator shown for it is only a blink of motion that reads as a glitch. This is the delay
/// Material's content loading guidance, Apple's activity indicator guidance and React Suspense
/// transitions all settle on.
pub(crate) const SHOW_AFTER: Duration = Duration::from_millis(300);
/// How long an indicator stays once it shows, even when the work ends sooner.
///
/// An indicator that appears and vanishes a few frames later flickers just like one shown for
/// instant work; half a second is long enough to be read as a deliberate state.
pub(crate) const SHOW_AT_LEAST: Duration = Duration::from_millis(500);
/// Where an indicator is in its life.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
enum Phase {
/// Nothing is loading and nothing shows.
#[default]
Idle,
/// Work started at this time; the indicator waits for [`SHOW_AFTER`].
Waiting(Duration),
/// The indicator has shown since this time.
Shown(Duration),
}
/// Whether a loading indicator shows right now, given when work started and ended.
///
/// Feed it whether the work is still busy on every frame with the frame's time
/// ([`PaintCx::now`](crate::widget::PaintCx::now)); it answers whether to draw the indicator and
/// when the answer changes next, so the widget can ask for a frame then. Work shorter than
/// [`SHOW_AFTER`] never shows it; once shown it stays [`SHOW_AT_LEAST`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(crate) struct DelayedIndicator {
phase: Phase,
}
impl DelayedIndicator {
/// Moves on to `now` with the work `busy` or not, and returns whether the indicator shows.
pub(crate) fn update(&mut self, busy: bool, now: Duration) -> bool {
self.phase = match (self.phase, busy) {
(Phase::Idle, false) => Phase::Idle,
(Phase::Idle, true) => Self::waiting(now, now),
(Phase::Waiting(since), true) => Self::waiting(since, now),
// Finished before anyone noticed: nothing was shown, nothing is.
(Phase::Waiting(_), false) => Phase::Idle,
(Phase::Shown(since), true) => Phase::Shown(since),
(Phase::Shown(since), false) if now < since + SHOW_AT_LEAST => Phase::Shown(since),
(Phase::Shown(_), false) => Phase::Idle,
};
self.is_shown()
}
fn waiting(since: Duration, now: Duration) -> Phase {
// The minimum counts from the frame that first draws the indicator: a frame that came
// late must not shorten how long it is on screen.
if now >= since + SHOW_AFTER { Phase::Shown(now) } else { Phase::Waiting(since) }
}
/// Whether the indicator shows, as of the last [`DelayedIndicator::update`].
pub(crate) fn is_shown(self) -> bool {
matches!(self.phase, Phase::Shown(_))
}
/// Whether nothing is waiting or showing, so the state can be forgotten.
pub(crate) fn is_idle(self) -> bool {
self.phase == Phase::Idle
}
/// Hides the indicator at once, e.g. when the work failed and its error takes over.
pub(crate) fn cancel(&mut self) {
self.phase = Phase::Idle;
}
/// How long after `now` the answer can change while the work stays as it was last updated:
/// when a waiting indicator is due, or when a shown one may hide.
pub(crate) fn next_change(self, busy: bool, now: Duration) -> Option<Duration> {
match self.phase {
Phase::Waiting(since) => Some((since + SHOW_AFTER).saturating_sub(now)),
Phase::Shown(since) if !busy => Some((since + SHOW_AT_LEAST).saturating_sub(now)),
Phase::Idle | Phase::Shown(_) => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn ms(value: u64) -> Duration {
Duration::from_millis(value)
}
#[test]
fn quick_work_never_shows() {
let mut indicator = DelayedIndicator::default();
assert!(!indicator.update(true, ms(1000)));
assert_eq!(indicator.next_change(true, ms(1000)), Some(ms(300)));
assert!(!indicator.update(true, ms(1299)));
assert!(!indicator.update(false, ms(1299)));
assert!(indicator.is_idle());
assert_eq!(indicator.next_change(false, ms(1300)), None);
}
#[test]
fn slow_work_shows_after_the_delay_and_stays_the_minimum() {
let mut indicator = DelayedIndicator::default();
indicator.update(true, ms(0));
assert!(indicator.update(true, ms(300)));
// The work ends at 350 ms; the indicator stays until 800 ms.
assert!(indicator.update(false, ms(350)));
assert_eq!(indicator.next_change(false, ms(350)), Some(ms(450)));
assert!(indicator.update(false, ms(799)));
assert!(!indicator.update(false, ms(800)));
assert!(indicator.is_idle());
}
#[test]
fn long_work_shows_for_as_long_as_it_runs() {
let mut indicator = DelayedIndicator::default();
indicator.update(true, ms(0));
assert!(indicator.update(true, ms(300)));
assert!(indicator.update(true, ms(2000)));
assert_eq!(indicator.next_change(true, ms(2000)), None);
assert!(!indicator.update(false, ms(2000)));
}
#[test]
fn new_work_while_lingering_keeps_it_shown_and_cancel_hides_at_once() {
let mut indicator = DelayedIndicator::default();
indicator.update(true, ms(0));
indicator.update(true, ms(300));
indicator.update(false, ms(400));
assert!(indicator.update(true, ms(900)));
indicator.cancel();
assert!(!indicator.is_shown());
assert!(!indicator.update(false, ms(901)));
}
}