use std::cell::{Cell, RefCell};
use std::sync::Arc;
pub const MIN_ZOOM_LEVEL: i8 = -8;
pub const MAX_ZOOM_LEVEL: i8 = 8;
pub const ZOOM_STEP: f32 = 1.2;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Zoom {
pub k: f32,
pub os: f32,
pub level: i8,
}
impl Zoom {
pub const DEFAULT: Zoom = Zoom {
k: 1.0,
os: 1.0,
level: 0,
};
pub fn app(&self) -> f32 {
app_scale(self.level)
}
}
impl Default for Zoom {
fn default() -> Self {
Self::DEFAULT
}
}
pub fn app_scale(level: i8) -> f32 {
ZOOM_STEP.powi(i32::from(clamp_level(level)))
}
pub fn clamp_level(level: i8) -> i8 {
level.clamp(MIN_ZOOM_LEVEL, MAX_ZOOM_LEVEL)
}
struct Subscriber {
id: u64,
notify: Arc<dyn Fn() + Send + Sync>,
}
thread_local! {
static CURRENT: Cell<Zoom> = const { Cell::new(Zoom::DEFAULT) };
static PENDING_LEVEL: Cell<Option<i8>> = const { Cell::new(None) };
static SUBSCRIBERS: RefCell<Vec<Subscriber>> = const { RefCell::new(Vec::new()) };
static NEXT_SUBSCRIBER_ID: Cell<u64> = const { Cell::new(1) };
}
pub fn zoom() -> Zoom {
CURRENT.with(Cell::get)
}
pub(crate) fn publish(zoom: Zoom) -> bool {
if CURRENT.with(Cell::get) == zoom {
return false;
}
CURRENT.with(|current| current.set(zoom));
SUBSCRIBERS.with_borrow(|subscribers| {
for subscriber in subscribers {
(subscriber.notify)();
}
!subscribers.is_empty()
})
}
pub fn set_zoom_level(level: i8) {
PENDING_LEVEL.with(|pending| pending.set(Some(clamp_level(level))));
}
pub(crate) fn take_pending_level() -> Option<i8> {
PENDING_LEVEL.with(Cell::take)
}
#[doc(hidden)]
pub fn reset_zoom() {
CURRENT.with(|current| current.set(Zoom::DEFAULT));
PENDING_LEVEL.with(|pending| pending.set(None));
SUBSCRIBERS.with_borrow_mut(Vec::clear);
}
pub struct ZoomSubscription {
id: u64,
}
pub fn subscribe(notify: Arc<dyn Fn() + Send + Sync>) -> ZoomSubscription {
let id = NEXT_SUBSCRIBER_ID.with(|next| {
let id = next.get();
next.set(id + 1);
id
});
SUBSCRIBERS.with_borrow_mut(|subscribers| subscribers.push(Subscriber { id, notify }));
ZoomSubscription { id }
}
impl Drop for ZoomSubscription {
fn drop(&mut self) {
SUBSCRIBERS.with_borrow_mut(|subscribers| {
subscribers.retain(|subscriber| subscriber.id != self.id);
});
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct ZoomHandle {
_private: (),
}
impl ZoomHandle {
pub fn get(&self) -> Zoom {
zoom()
}
pub fn set_level(&self, level: i8) {
set_zoom_level(level);
}
pub fn zoom_in(&self) {
set_zoom_level(self.current_level().saturating_add(1));
}
pub fn zoom_out(&self) {
set_zoom_level(self.current_level().saturating_sub(1));
}
pub fn reset(&self) {
set_zoom_level(0);
}
fn current_level(&self) -> i8 {
PENDING_LEVEL
.with(Cell::get)
.unwrap_or_else(|| zoom().level)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fr43_app_scale_is_one_point_two_to_the_level() {
assert_eq!(app_scale(0), 1.0);
assert!((app_scale(2) - 1.44).abs() < 1e-6);
assert!((app_scale(-1) - 1.0 / 1.2).abs() < 1e-6);
assert_eq!(app_scale(20), app_scale(MAX_ZOOM_LEVEL));
}
#[test]
fn fr43_two_steps_in_one_handler_are_two_steps() {
reset_zoom();
let handle = ZoomHandle::default();
handle.zoom_in();
handle.zoom_in();
assert_eq!(take_pending_level(), Some(2));
assert_eq!(take_pending_level(), None);
handle.set_level(40);
assert_eq!(take_pending_level(), Some(MAX_ZOOM_LEVEL));
reset_zoom();
}
#[test]
fn fr43_a_report_wakes_only_on_change() {
reset_zoom();
let woken = Arc::new(std::sync::atomic::AtomicUsize::new(0));
let counter = woken.clone();
let _subscription = subscribe(Arc::new(move || {
counter.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
}));
let report = Zoom {
k: 1.8,
os: 1.25,
level: 2,
};
assert!(publish(report));
assert!(!publish(report), "the same report twice is one change");
assert_eq!(woken.load(std::sync::atomic::Ordering::SeqCst), 1);
assert_eq!(zoom(), report);
reset_zoom();
}
}