1use std::cell::{Cell, RefCell};
20use std::sync::Arc;
21
22pub const MIN_ZOOM_LEVEL: i8 = -8;
24pub const MAX_ZOOM_LEVEL: i8 = 8;
26
27pub const ZOOM_STEP: f32 = 1.2;
29
30#[derive(Clone, Copy, Debug, PartialEq)]
32pub struct Zoom {
33 pub k: f32,
35 pub os: f32,
37 pub level: i8,
39}
40
41impl Zoom {
42 pub const DEFAULT: Zoom = Zoom {
44 k: 1.0,
45 os: 1.0,
46 level: 0,
47 };
48
49 pub fn app(&self) -> f32 {
51 app_scale(self.level)
52 }
53}
54
55impl Default for Zoom {
56 fn default() -> Self {
57 Self::DEFAULT
58 }
59}
60
61pub fn app_scale(level: i8) -> f32 {
63 ZOOM_STEP.powi(i32::from(clamp_level(level)))
64}
65
66pub fn clamp_level(level: i8) -> i8 {
68 level.clamp(MIN_ZOOM_LEVEL, MAX_ZOOM_LEVEL)
69}
70
71struct Subscriber {
72 id: u64,
73 notify: Arc<dyn Fn() + Send + Sync>,
74}
75
76thread_local! {
77 static CURRENT: Cell<Zoom> = const { Cell::new(Zoom::DEFAULT) };
78 static PENDING_LEVEL: Cell<Option<i8>> = const { Cell::new(None) };
80 static SUBSCRIBERS: RefCell<Vec<Subscriber>> = const { RefCell::new(Vec::new()) };
81 static NEXT_SUBSCRIBER_ID: Cell<u64> = const { Cell::new(1) };
82}
83
84pub fn zoom() -> Zoom {
86 CURRENT.with(Cell::get)
87}
88
89pub(crate) fn publish(zoom: Zoom) -> bool {
94 if CURRENT.with(Cell::get) == zoom {
95 return false;
96 }
97 CURRENT.with(|current| current.set(zoom));
98 SUBSCRIBERS.with_borrow(|subscribers| {
99 for subscriber in subscribers {
100 (subscriber.notify)();
101 }
102 !subscribers.is_empty()
103 })
104}
105
106pub fn set_zoom_level(level: i8) {
109 PENDING_LEVEL.with(|pending| pending.set(Some(clamp_level(level))));
110}
111
112pub(crate) fn take_pending_level() -> Option<i8> {
114 PENDING_LEVEL.with(Cell::take)
115}
116
117#[doc(hidden)]
120pub fn reset_zoom() {
121 CURRENT.with(|current| current.set(Zoom::DEFAULT));
122 PENDING_LEVEL.with(|pending| pending.set(None));
123 SUBSCRIBERS.with_borrow_mut(Vec::clear);
124}
125
126pub struct ZoomSubscription {
128 id: u64,
129}
130
131pub fn subscribe(notify: Arc<dyn Fn() + Send + Sync>) -> ZoomSubscription {
133 let id = NEXT_SUBSCRIBER_ID.with(|next| {
134 let id = next.get();
135 next.set(id + 1);
136 id
137 });
138 SUBSCRIBERS.with_borrow_mut(|subscribers| subscribers.push(Subscriber { id, notify }));
139 ZoomSubscription { id }
140}
141
142impl Drop for ZoomSubscription {
143 fn drop(&mut self) {
144 SUBSCRIBERS.with_borrow_mut(|subscribers| {
145 subscribers.retain(|subscriber| subscriber.id != self.id);
146 });
147 }
148}
149
150#[derive(Clone, Copy, Debug, Default)]
152pub struct ZoomHandle {
153 _private: (),
154}
155
156impl ZoomHandle {
157 pub fn get(&self) -> Zoom {
159 zoom()
160 }
161
162 pub fn set_level(&self, level: i8) {
164 set_zoom_level(level);
165 }
166
167 pub fn zoom_in(&self) {
169 set_zoom_level(self.current_level().saturating_add(1));
170 }
171
172 pub fn zoom_out(&self) {
174 set_zoom_level(self.current_level().saturating_sub(1));
175 }
176
177 pub fn reset(&self) {
179 set_zoom_level(0);
180 }
181
182 fn current_level(&self) -> i8 {
185 PENDING_LEVEL
186 .with(Cell::get)
187 .unwrap_or_else(|| zoom().level)
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194
195 #[test]
196 fn fr43_app_scale_is_one_point_two_to_the_level() {
197 assert_eq!(app_scale(0), 1.0);
198 assert!((app_scale(2) - 1.44).abs() < 1e-6);
199 assert!((app_scale(-1) - 1.0 / 1.2).abs() < 1e-6);
200 assert_eq!(app_scale(20), app_scale(MAX_ZOOM_LEVEL));
201 }
202
203 #[test]
204 fn fr43_two_steps_in_one_handler_are_two_steps() {
205 reset_zoom();
206 let handle = ZoomHandle::default();
207 handle.zoom_in();
208 handle.zoom_in();
209 assert_eq!(take_pending_level(), Some(2));
210 assert_eq!(take_pending_level(), None);
211 handle.set_level(40);
212 assert_eq!(take_pending_level(), Some(MAX_ZOOM_LEVEL));
213 reset_zoom();
214 }
215
216 #[test]
217 fn fr43_a_report_wakes_only_on_change() {
218 reset_zoom();
219 let woken = Arc::new(std::sync::atomic::AtomicUsize::new(0));
220 let counter = woken.clone();
221 let _subscription = subscribe(Arc::new(move || {
222 counter.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
223 }));
224 let report = Zoom {
225 k: 1.8,
226 os: 1.25,
227 level: 2,
228 };
229 assert!(publish(report));
230 assert!(!publish(report), "the same report twice is one change");
231 assert_eq!(woken.load(std::sync::atomic::Ordering::SeqCst), 1);
232 assert_eq!(zoom(), report);
233 reset_zoom();
234 }
235}