cranpose-services 0.1.2

Multiplatform system services for Cranpose (HTTP, URI, and OS integrations)
Documentation
use cranpose_core::{compositionLocalOf, CompositionLocal, CompositionLocalProvider};
use cranpose_macros::composable;
use std::cell::RefCell;

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SystemTheme {
    Light,
    Dark,
}

pub fn default_system_theme() -> SystemTheme {
    #[cfg(all(
        not(target_arch = "wasm32"),
        not(target_os = "android"),
        not(target_os = "ios")
    ))]
    {
        match dark_light::detect() {
            Ok(dark_light::Mode::Dark) => SystemTheme::Dark,
            _ => SystemTheme::Light,
        }
    }

    #[cfg(target_arch = "wasm32")]
    {
        web_sys::window()
            .and_then(|window| {
                window
                    .match_media("(prefers-color-scheme: dark)")
                    .ok()
                    .flatten()
            })
            .map(|query| {
                if query.matches() {
                    SystemTheme::Dark
                } else {
                    SystemTheme::Light
                }
            })
            .unwrap_or(SystemTheme::Light)
    }

    #[cfg(any(target_os = "android", target_os = "ios"))]
    {
        SystemTheme::Light
    }
}

pub fn local_system_theme() -> CompositionLocal<SystemTheme> {
    thread_local! {
        static LOCAL_SYSTEM_THEME: RefCell<Option<CompositionLocal<SystemTheme>>> = const { RefCell::new(None) };
    }

    LOCAL_SYSTEM_THEME.with(|cell| {
        let mut local = cell.borrow_mut();
        if local.is_none() {
            *local = Some(compositionLocalOf(default_system_theme));
        }
        local
            .as_ref()
            .expect("System theme composition local must be initialized")
            .clone()
    })
}

#[allow(non_snake_case)]
#[composable]
pub fn ProvideSystemTheme(theme: SystemTheme, content: impl FnOnce()) {
    let local = local_system_theme();
    CompositionLocalProvider(vec![local.provides(theme)], move || {
        content();
    });
}

#[allow(non_snake_case)]
#[composable]
pub fn isSystemInDarkTheme() -> bool {
    matches!(local_system_theme().current(), SystemTheme::Dark)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::run_test_composition;
    use cranpose_core::CompositionLocalProvider;
    use std::cell::RefCell;
    use std::rc::Rc;

    #[test]
    fn default_system_theme_returns_supported_variant() {
        assert!(matches!(
            default_system_theme(),
            SystemTheme::Light | SystemTheme::Dark
        ));
    }

    #[test]
    fn local_system_theme_can_be_overridden() {
        let local = local_system_theme();
        let captured = Rc::new(RefCell::new(None));

        {
            let captured = Rc::clone(&captured);
            let local_for_provider = local.clone();
            let local_for_read = local.clone();
            run_test_composition(move || {
                let captured = Rc::clone(&captured);
                let local_for_read = local_for_read.clone();
                CompositionLocalProvider(
                    vec![local_for_provider.provides(SystemTheme::Dark)],
                    move || {
                        *captured.borrow_mut() = Some(local_for_read.current());
                    },
                );
            });
        }

        assert_eq!(*captured.borrow(), Some(SystemTheme::Dark));
    }

    #[test]
    fn provide_system_theme_sets_current_theme() {
        let local = local_system_theme();
        let captured = Rc::new(RefCell::new(None));

        {
            let captured = Rc::clone(&captured);
            let local = local.clone();
            run_test_composition(move || {
                let captured = Rc::clone(&captured);
                let local = local.clone();
                ProvideSystemTheme(SystemTheme::Dark, move || {
                    *captured.borrow_mut() = Some(local.current());
                });
            });
        }

        assert_eq!(*captured.borrow(), Some(SystemTheme::Dark));
    }

    #[test]
    fn is_system_in_dark_theme_reads_current_theme() {
        let captured = Rc::new(RefCell::new(None));

        {
            let captured = Rc::clone(&captured);
            run_test_composition(move || {
                let captured = Rc::clone(&captured);
                ProvideSystemTheme(SystemTheme::Dark, move || {
                    *captured.borrow_mut() = Some(isSystemInDarkTheme());
                });
            });
        }

        assert_eq!(*captured.borrow(), Some(true));
    }
}