cranpose 0.1.149

Cranpose runtime and UI facade
Documentation
use std::{
    rc::Rc,
    sync::{
        Arc,
        atomic::{AtomicBool, AtomicU8, Ordering},
    },
};

use cranpose_services::{
    BatteryStatus, PowerCapabilities, PowerMonitor, PowerReading, power_state, publish_power_state,
    set_platform_power_monitor,
};
use wasm_bindgen::{JsCast, JsValue, closure::Closure};
use wasm_bindgen_futures::{JsFuture, spawn_local};

pub(crate) fn register() {
    set_platform_power_monitor(Arc::new(WebPowerMonitor));
}

pub(crate) fn start_battery_probe(request_frame: Rc<dyn Fn()>) {
    let Some(navigator) = web_sys::window().map(|window| window.navigator()) else {
        return;
    };
    let Some(get_battery) = get_battery_function(&navigator) else {
        return;
    };
    let promise = match get_battery.call0(navigator.as_ref()) {
        Ok(value) => value,
        Err(error) => {
            log::debug!("navigator.getBattery() call failed: {error:?}");
            return;
        }
    };
    let Ok(promise) = promise.dyn_into::<js_sys::Promise>() else {
        return;
    };
    BATTERY_SUPPORTED.store(true, Ordering::Release);
    publish_power_state(power_state());

    spawn_local(async move {
        let manager = match JsFuture::from(promise).await {
            Ok(value) => match value.dyn_into::<web_sys::BatteryManager>() {
                Ok(manager) => manager,
                Err(_) => return,
            },
            Err(error) => {
                log::debug!("navigator.getBattery() promise rejected: {error:?}");
                return;
            }
        };
        update_from_manager(&manager, &request_frame);
        for event_name in ["levelchange", "chargingchange"] {
            let manager_for_closure = manager.clone();
            let request_frame = request_frame.clone();
            let closure = Closure::wrap(Box::new(move || {
                update_from_manager(&manager_for_closure, &request_frame);
            }) as Box<dyn FnMut()>);
            let _ = manager
                .add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref());
            closure.forget();
        }
    });
}

fn get_battery_function(navigator: &web_sys::Navigator) -> Option<js_sys::Function> {
    let value = js_sys::Reflect::get(navigator.as_ref(), &JsValue::from_str("getBattery")).ok()?;
    value.dyn_into::<js_sys::Function>().ok()
}

fn update_from_manager(manager: &web_sys::BatteryManager, request_frame: &Rc<dyn Fn()>) {
    let percent = (manager.level() * 100.0).round().clamp(0.0, 100.0) as u8;
    BATTERY_PERCENT.store(percent, Ordering::Release);
    BATTERY_CHARGING.store(manager.charging(), Ordering::Release);
    BATTERY_RESOLVED.store(true, Ordering::Release);
    publish_power_state(power_state());
    request_frame();
}

static BATTERY_SUPPORTED: AtomicBool = AtomicBool::new(false);
static BATTERY_RESOLVED: AtomicBool = AtomicBool::new(false);
static BATTERY_PERCENT: AtomicU8 = AtomicU8::new(0);
static BATTERY_CHARGING: AtomicBool = AtomicBool::new(false);

struct WebPowerMonitor;

impl PowerMonitor for WebPowerMonitor {
    fn capabilities(&self) -> PowerCapabilities {
        PowerCapabilities {
            thermal: false,
            battery: BATTERY_SUPPORTED.load(Ordering::Acquire),
            background_restriction: false,
        }
    }

    fn battery_status(&self) -> PowerReading<BatteryStatus> {
        if !BATTERY_SUPPORTED.load(Ordering::Acquire) {
            return PowerReading::Unsupported;
        }
        if !BATTERY_RESOLVED.load(Ordering::Acquire) {
            return PowerReading::Unknown;
        }
        PowerReading::Known(BatteryStatus {
            percent: BATTERY_PERCENT.load(Ordering::Acquire),
            charging: BATTERY_CHARGING.load(Ordering::Acquire),
        })
    }
}