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 WINDOW_ZOOMS: RefCell<Vec<(u32, Zoom)>> = const { RefCell::new(Vec::new()) };
84 static WINDOW_SUBSCRIBERS: RefCell<Vec<(u32, Subscriber)>> = const { RefCell::new(Vec::new()) };
85 static NEXT_SUBSCRIBER_ID: Cell<u64> = const { Cell::new(1) };
86}
87
88pub fn zoom() -> Zoom {
92 match crate::windows::current_window_id() {
93 0 => CURRENT.with(Cell::get),
94 window => zoom_of(window),
95 }
96}
97
98pub fn zoom_of(window: u32) -> Zoom {
101 WINDOW_ZOOMS.with_borrow(|zooms| {
102 zooms
103 .iter()
104 .find(|(id, _)| *id == window)
105 .map_or(Zoom::DEFAULT, |(_, zoom)| *zoom)
106 })
107}
108
109pub(crate) fn publish_window(window: u32, zoom: Zoom) -> bool {
112 let changed = WINDOW_ZOOMS.with_borrow_mut(|zooms| {
113 match zooms.iter_mut().find(|(id, _)| *id == window) {
114 Some(entry) => {
115 if entry.1 == zoom {
116 return false;
117 }
118 entry.1 = zoom;
119 true
120 }
121 None => {
122 zooms.push((window, zoom));
123 true
124 }
125 }
126 });
127 if !changed {
128 return false;
129 }
130 WINDOW_SUBSCRIBERS.with_borrow(|subscribers| {
131 let mut woke = false;
132 for (id, subscriber) in subscribers {
133 if *id == window {
134 (subscriber.notify)();
135 woke = true;
136 }
137 }
138 woke
139 })
140}
141
142pub(crate) fn window_state_len() -> usize {
144 WINDOW_ZOOMS.with_borrow(Vec::len) + WINDOW_SUBSCRIBERS.with_borrow(Vec::len)
145}
146
147pub(crate) fn forget_window(window: u32) {
149 WINDOW_ZOOMS.with_borrow_mut(|zooms| zooms.retain(|(id, _)| *id != window));
150}
151
152pub struct WindowZoomSubscription {
154 id: u64,
155}
156
157pub fn subscribe_window(
159 window: u32,
160 notify: Arc<dyn Fn() + Send + Sync>,
161) -> WindowZoomSubscription {
162 let id = next_subscriber_id();
163 WINDOW_SUBSCRIBERS.with_borrow_mut(|subscribers| {
164 subscribers.push((window, Subscriber { id, notify }));
165 });
166 WindowZoomSubscription { id }
167}
168
169impl Drop for WindowZoomSubscription {
170 fn drop(&mut self) {
171 WINDOW_SUBSCRIBERS.with_borrow_mut(|subscribers| {
172 subscribers.retain(|(_, subscriber)| subscriber.id != self.id);
173 });
174 }
175}
176
177fn next_subscriber_id() -> u64 {
178 NEXT_SUBSCRIBER_ID.with(|next| {
179 let id = next.get();
180 next.set(id + 1);
181 id
182 })
183}
184
185pub(crate) fn publish(zoom: Zoom) -> bool {
190 if CURRENT.with(Cell::get) == zoom {
191 return false;
192 }
193 CURRENT.with(|current| current.set(zoom));
194 SUBSCRIBERS.with_borrow(|subscribers| {
195 for subscriber in subscribers {
196 (subscriber.notify)();
197 }
198 !subscribers.is_empty()
199 })
200}
201
202pub fn set_zoom_level(level: i8) {
207 let level = clamp_level(level);
208 match crate::windows::current_window_id() {
209 0 => PENDING_LEVEL.with(|pending| pending.set(Some(level))),
210 window => crate::windows::queue_zoom_level(window, level),
211 }
212}
213
214pub(crate) fn take_pending_level() -> Option<i8> {
216 PENDING_LEVEL.with(Cell::take)
217}
218
219#[doc(hidden)]
222pub fn reset_zoom() {
223 CURRENT.with(|current| current.set(Zoom::DEFAULT));
224 PENDING_LEVEL.with(|pending| pending.set(None));
225 SUBSCRIBERS.with_borrow_mut(Vec::clear);
226 WINDOW_ZOOMS.with_borrow_mut(Vec::clear);
227 WINDOW_SUBSCRIBERS.with_borrow_mut(Vec::clear);
228}
229
230pub struct ZoomSubscription {
232 id: u64,
233}
234
235pub fn subscribe(notify: Arc<dyn Fn() + Send + Sync>) -> ZoomSubscription {
237 let id = next_subscriber_id();
238 SUBSCRIBERS.with_borrow_mut(|subscribers| subscribers.push(Subscriber { id, notify }));
239 ZoomSubscription { id }
240}
241
242impl Drop for ZoomSubscription {
243 fn drop(&mut self) {
244 SUBSCRIBERS.with_borrow_mut(|subscribers| {
245 subscribers.retain(|subscriber| subscriber.id != self.id);
246 });
247 }
248}
249
250#[derive(Clone, Copy, Debug, Default)]
252pub struct ZoomHandle {
253 _private: (),
254}
255
256impl ZoomHandle {
257 pub fn get(&self) -> Zoom {
259 zoom()
260 }
261
262 pub fn set_level(&self, level: i8) {
264 set_zoom_level(level);
265 }
266
267 pub fn zoom_in(&self) {
269 set_zoom_level(self.current_level().saturating_add(1));
270 }
271
272 pub fn zoom_out(&self) {
274 set_zoom_level(self.current_level().saturating_sub(1));
275 }
276
277 pub fn reset(&self) {
279 set_zoom_level(0);
280 }
281
282 fn current_level(&self) -> i8 {
285 match crate::windows::current_window_id() {
286 0 => PENDING_LEVEL
287 .with(Cell::get)
288 .unwrap_or_else(|| zoom().level),
289 window => {
290 crate::windows::pending_zoom_level(window).unwrap_or_else(|| zoom_of(window).level)
291 }
292 }
293 }
294}
295
296#[cfg(test)]
297mod tests {
298 use super::*;
299
300 #[test]
301 fn fr43_app_scale_is_one_point_two_to_the_level() {
302 assert_eq!(app_scale(0), 1.0);
303 assert!((app_scale(2) - 1.44).abs() < 1e-6);
304 assert!((app_scale(-1) - 1.0 / 1.2).abs() < 1e-6);
305 assert_eq!(app_scale(20), app_scale(MAX_ZOOM_LEVEL));
306 }
307
308 #[test]
309 fn fr43_two_steps_in_one_handler_are_two_steps() {
310 reset_zoom();
311 let handle = ZoomHandle::default();
312 handle.zoom_in();
313 handle.zoom_in();
314 assert_eq!(take_pending_level(), Some(2));
315 assert_eq!(take_pending_level(), None);
316 handle.set_level(40);
317 assert_eq!(take_pending_level(), Some(MAX_ZOOM_LEVEL));
318 reset_zoom();
319 }
320
321 #[test]
322 fn fr43_a_report_wakes_only_on_change() {
323 reset_zoom();
324 let woken = Arc::new(std::sync::atomic::AtomicUsize::new(0));
325 let counter = woken.clone();
326 let _subscription = subscribe(Arc::new(move || {
327 counter.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
328 }));
329 let report = Zoom {
330 k: 1.8,
331 os: 1.25,
332 level: 2,
333 };
334 assert!(publish(report));
335 assert!(!publish(report), "the same report twice is one change");
336 assert_eq!(woken.load(std::sync::atomic::Ordering::SeqCst), 1);
337 assert_eq!(zoom(), report);
338 reset_zoom();
339 }
340}