aura-gui 0.3.0

UI nativa acelerada por GPU para escritorio, Android e iOS, en un solo crate: estilo tipo Tailwind, componentes, sombras, animaciones y agentes de IA. Reposo a cero. Licencia AGPL-3.0 o comercial.
//! Estado reactivo por clave para la API de closures: `ui.state("clics", 0)`.
//!
//! Un `State<T>` es un asa barata (`Rc<RefCell<T>>`) que sobrevive a los frames. Los manejadores que devuelve
//! (`update`, `set`, `toggle`) mutan el valor y piden un frame nuevo, así que la app no necesita `cx.notify()`.

use crate::view::interactivity::{ClickEvent, EventContext};
use std::any::Any;
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;

/// Valor reactivo con una clave estable dentro de la app.
pub struct State<T> {
    inner: Rc<RefCell<T>>,
    key: &'static str,
}

impl<T> Clone for State<T> {
    fn clone(&self) -> Self {
        Self { inner: Rc::clone(&self.inner), key: self.key }
    }
}

impl<T> State<T> {
    /// Clave con la que se creó; sirve para identificadores estables (foco, accesibilidad, listas).
    pub fn key(&self) -> &'static str {
        self.key
    }

    /// Lee el valor sin clonarlo.
    pub fn read<R>(&self, read: impl FnOnce(&T) -> R) -> R {
        read(&self.inner.borrow())
    }

    /// Muta el valor.
    pub fn write(&self, write: impl FnOnce(&mut T)) {
        write(&mut self.inner.borrow_mut());
    }

    /// Muta el valor y devuelve un resultado, sin clonarlo.
    pub fn with_mut<R>(&self, body: impl FnOnce(&mut T) -> R) -> R {
        body(&mut self.inner.borrow_mut())
    }

    /// Clic que muta el valor y redibuja. Se puede repetir: `state.update(|n| *n += 1)`.
    pub fn update(&self, change: impl Fn(&mut T) + 'static) -> impl Fn(&ClickEvent, &mut EventContext) + 'static
    where
        T: 'static,
    {
        let state = self.clone();
        move |_, cx| {
            state.write(|value| change(value));
            cx.notify();
        }
    }
}

impl<T: Clone> State<T> {
    /// Copia del valor actual, para pintarlo: `state.get()`.
    pub fn get(&self) -> T {
        self.inner.borrow().clone()
    }

    /// Clic que asigna un valor y redibuja.
    pub fn set(&self, value: T) -> impl Fn(&ClickEvent, &mut EventContext) + 'static
    where
        T: 'static,
    {
        let state = self.clone();
        move |_, cx| {
            state.write(|current| *current = value.clone());
            cx.notify();
        }
    }
}

impl State<bool> {
    /// Clic que invierte el valor y redibuja.
    pub fn toggle(&self) -> impl Fn(&ClickEvent, &mut EventContext) + 'static {
        let state = self.clone();
        move |_, cx| {
            state.write(|value| *value = !*value);
            cx.notify();
        }
    }
}

/// Valores de una app de closure, indexados por clave y tipo.
#[derive(Default)]
pub(crate) struct StateStore {
    values: HashMap<&'static str, Rc<dyn Any>>,
}

impl StateStore {
    /// Estado existente de `key`, si lo hay y con el mismo tipo.
    pub fn get<T: 'static>(&self, key: &'static str) -> Option<State<T>> {
        let existing = self.values.get(key)?;
        let Some(cell) = existing.clone().downcast::<RefCell<T>>().ok() else {
            panic!("el estado `{key}` se pidió como `{}` pero ya existía como otro tipo", std::any::type_name::<T>());
        };
        Some(State { inner: cell, key })
    }

    /// Guarda un valor nuevo y devuelve su asa.
    pub fn insert<T: 'static>(&mut self, key: &'static str, value: T) -> State<T> {
        let cell = Rc::new(RefCell::new(value));
        self.values.insert(key, cell.clone());
        State { inner: cell, key }
    }

    /// Devuelve el estado de `key` o lo crea con `default` la primera vez.
    pub fn get_or_insert<T: 'static>(&mut self, key: &'static str, default: T) -> State<T> {
        self.get_or_insert_with(key, || default)
    }

    /// Como [`Self::get_or_insert`], pero el valor inicial solo se construye si la clave no existía.
    pub fn get_or_insert_with<T: 'static>(&mut self, key: &'static str, default: impl FnOnce() -> T) -> State<T> {
        if let Some(existing) = self.values.get(key) {
            let Some(cell) = existing.clone().downcast::<RefCell<T>>().ok() else {
                panic!(
                    "el estado `{key}` se pidió como `{}` pero ya existía como otro tipo",
                    std::any::type_name::<T>()
                );
            };
            return State { inner: cell, key };
        }
        let cell = Rc::new(RefCell::new(default()));
        self.values.insert(key, cell.clone());
        State { inner: cell, key }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn el_estado_se_conserva_y_se_lee() {
        let mut store = StateStore::default();
        let count = store.get_or_insert("clics", 0u32);
        count.write(|value| *value += 1);
        assert_eq!(count.get(), 1);
        let same = store.get_or_insert("clics", 99u32);
        assert_eq!(same.get(), 1, "la clave devuelve el mismo valor, no el default");
    }

    #[test]
    #[should_panic(expected = "ya existía como otro tipo")]
    fn una_clave_con_otro_tipo_avisa_claro() {
        let mut store = StateStore::default();
        let _ = store.get_or_insert("dato", 1u32);
        let _ = store.get_or_insert("dato", String::new());
    }
}