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
use web_time::{Duration, Instant};
/// How often a changing tree is published while nobody uses it: a screen
/// reader reads the tree when the person touches, swipes or acts, so
/// changes such as a ticker's need no more than this.
pub(crate) const ACCESSIBILITY_PUBLISH_INTERVAL: Duration = Duration::from_secs(1);
/// How often a changing tree is published while a reader is in use, so the
/// person never explores a stale screen.
pub(crate) const ACCESSIBILITY_INTERACTIVE_INTERVAL: Duration = Duration::from_millis(100);
/// How long a read or an action keeps publishes at the interactive interval.
pub(crate) const ACCESSIBILITY_INTERACTION_WINDOW: Duration = Duration::from_secs(2);
/// The longest wait between published trees while nothing reads them.
pub(crate) const ACCESSIBILITY_UNREAD_PUBLISH_INTERVAL: Duration = Duration::from_secs(4);
pub(crate) struct AccessibilityPublishPolicy {
enabled: bool,
last_publish: Option<Instant>,
pending_deadline: Option<Instant>,
/// The wait after a publish while nobody interacts: the publish
/// interval, or longer while published trees go unread.
interval: Duration,
/// Until when the reader counts as in use.
interactive_until: Option<Instant>,
/// Whether a reader read since the last tree was sent.
read_since_publish: bool,
published_once: bool,
}
impl AccessibilityPublishPolicy {
pub(crate) fn new() -> Self {
Self {
enabled: false,
last_publish: None,
pending_deadline: None,
interval: ACCESSIBILITY_PUBLISH_INTERVAL,
interactive_until: None,
read_since_publish: false,
published_once: false,
}
}
pub(crate) fn update_enabled(&mut self, enabled: bool) -> bool {
let became_enabled = enabled && !self.enabled;
if became_enabled {
self.last_publish = None;
self.interval = ACCESSIBILITY_PUBLISH_INTERVAL;
self.published_once = false;
}
if !enabled {
self.pending_deadline = None;
}
self.enabled = enabled;
became_enabled
}
/// A reader read the tree or acted on it at `now`: changes publish at
/// the interactive interval for [`ACCESSIBILITY_INTERACTION_WINDOW`].
pub(crate) fn note_read(&mut self, now: Instant) {
self.read_since_publish = true;
self.interval = ACCESSIBILITY_PUBLISH_INTERVAL;
self.interactive_until = Some(now + ACCESSIBILITY_INTERACTION_WINDOW);
}
fn try_begin_publish(&mut self, now: Instant) -> bool {
if !self.enabled {
return false;
}
let wait = if self.interactive_until.is_some_and(|until| now < until) {
ACCESSIBILITY_INTERACTIVE_INTERVAL
} else {
self.interval
};
match self.last_publish {
Some(last) if now.duration_since(last) < wait => {
self.pending_deadline = Some(last + wait);
false
}
_ => {
self.pending_deadline = None;
self.last_publish = Some(now);
true
}
}
}
/// Whether a bridge that publishes only a tree that changed publishes at
/// `now`: never while the tree is as it was last published, at once when
/// a reader needs the change now, and otherwise when the interval allows.
/// A wake is armed only while a change waits for the interval, so an idle
/// app gets no frame for a tree it already published.
pub(crate) fn try_publish_change(
&mut self,
now: Instant,
changed: bool,
at_once: bool,
) -> bool {
if !changed {
return false;
}
if at_once {
self.pending_deadline = None;
self.last_publish = Some(now);
return true;
}
self.try_begin_publish(now)
}
/// A tree was sent to the platform. When `backs_off_unread`, the tree
/// before it went unread and the reader is not in use, the wait before
/// the next one doubles, up to [`ACCESSIBILITY_UNREAD_PUBLISH_INTERVAL`].
/// A platform backs off only where it reports reads and no screen reader
/// runs: a screen reader reads only when the person uses it.
pub(crate) fn published(&mut self, backs_off_unread: bool) {
let in_use = self
.interactive_until
.zip(self.last_publish)
.is_some_and(|(until, last)| last < until);
let unread = self.published_once && !self.read_since_publish;
self.interval = if backs_off_unread && unread && !in_use {
(self.interval * 2).min(ACCESSIBILITY_UNREAD_PUBLISH_INTERVAL)
} else {
ACCESSIBILITY_PUBLISH_INTERVAL
};
self.read_since_publish = false;
self.published_once = true;
}
pub(crate) fn wake_deadline(&self) -> Option<Instant> {
self.pending_deadline
}
}
#[cfg(test)]
#[path = "tests/accessibility_publish_policy_tests.rs"]
mod tests;