aura-gui 0.3.5

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()`.
//! Cuando se crea con `ui.persisted("clave", valor)`, cada escritura se guarda también en el [`Store`] de la app.

use crate::core::platform::Store;
use crate::core::tasks::BackgroundExecutor;
use crate::view::interactivity::{ClickEvent, EventContext};
pub use crate::view::persist::PersistValue;
use rustc_hash::FxHashMap;
use serde_json::Value;
use std::any::Any;
use std::cell::RefCell;
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,
    saver: Option<Rc<dyn Fn(&T)>>,
}

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

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())
    }

    fn flush_save(&self) {
        if let Some(saver) = &self.saver {
            saver(&self.inner.borrow());
        }
    }

    /// Muta el valor (y, si es persistente, lo guarda).
    pub fn write(&self, write: impl FnOnce(&mut T)) {
        write(&mut self.inner.borrow_mut());
        self.flush_save();
    }

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

    /// 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();
        }
    }
}

impl State<String> {
    /// Extrae el texto sin espacios a los lados y vacía el estado si no estaba vacío.
    ///
    /// Pensado para formularios rápidos: `if let Some(texto) = nueva.take_trimmed() { ... }`.
    pub fn take_trimmed(&self) -> Option<String> {
        let trimmed = self.read(|text| text.trim().to_owned());
        if trimmed.is_empty() {
            return None;
        }
        self.write(String::clear);
        Some(trimmed)
    }
}

impl<T: 'static> State<Vec<T>> {
    /// Añade un elemento a la lista.
    pub fn push(&self, item: T) {
        self.write(|items| items.push(item));
    }

    /// Clic que elimina el elemento `index` (si existe) y redibuja.
    pub fn remove_at(&self, index: usize) -> impl Fn(&ClickEvent, &mut EventContext) + 'static {
        self.update(move |items| {
            if index < items.len() {
                items.remove(index);
            }
        })
    }
}

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

impl StateStore {
    pub fn with_store(store: Option<Store>) -> Self {
        Self { values: FxHashMap::default(), savers: FxHashMap::default(), store, cache: None }
    }

    fn saver_for<T: 'static>(&self, key: &'static str) -> Option<Rc<dyn Fn(&T)>> {
        self.savers.get(key)?.clone().downcast::<Rc<dyn Fn(&T)>>().ok().map(|rc| (*rc).clone())
    }

    /// 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, saver: self.saver_for(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, saver: None }
    }

    /// 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(state) = self.get::<T>(key) {
            return state;
        }
        self.insert(key, default())
    }

    /// Crea o recupera un estado persistido en el [`Store`] de la app. La escritura en disco ocurre en el pool.
    pub fn persisted<T: PersistValue>(
        &mut self,
        key: &'static str,
        default: impl FnOnce() -> T,
        background: &BackgroundExecutor,
    ) -> State<T> {
        if let Some(state) = self.get::<T>(key) {
            return state;
        }
        let loaded = self.store.as_ref().and_then(|store| {
            let root = self.cache.get_or_insert_with(|| store.load());
            root.get(key).and_then(T::from_json)
        });
        let initial = loaded.unwrap_or_else(default);
        let cell = Rc::new(RefCell::new(initial));
        self.values.insert(key, cell.clone());
        let saver: Option<Rc<dyn Fn(&T)>> = self.store.clone().map(|store| {
            let pool = background.clone();
            let cb: Rc<dyn Fn(&T)> = Rc::new(move |val: &T| {
                let json = val.to_json();
                let store = store.clone();
                pool.spawn(move || {
                    let _ = store.update(|root| root[key] = json);
                });
            });
            self.savers.insert(key, Rc::new(cb.clone()));
            cb
        });
        State { inner: cell, key, saver }
    }
}

#[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());
    }
}