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.
//! Vistas con estado: `ViewContext` y el trait interno `Component`, equivalentes a `Entity<T>` de GPUI.

use crate::core::{Action, Animation, Easing, EntityContext, FocusHandle, Handle, Lerp, Size};
use crate::view::any_element::AnyElement;
use crate::view::div::div;
use crate::view::element::IntoElement;
use crate::view::interactivity::{ClickEvent, Effects, EventContext};
use crate::view::theme::Theme;
use std::any::Any;
use std::ops::{Deref, DerefMut};
use std::time::{Duration, Instant};

/// Segundos que dura la fase de `phase`: un día, para que no se reinicie nunca mientras la app está abierta.
const PHASE_SECONDS: f32 = 86_400.0;

/// Trait interno que conecta las apps con el motor. No se implementa a mano: se usa la API de closure
/// (`run_app`) o, en apps que ya tienen estado en entidades, la macro [`crate::component!`].
#[doc(hidden)]
pub trait Component: Sized + 'static {
    fn view(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement;
}

pub struct ViewContext<'a, 'b, T> {
    pub theme: &'a Theme,
    pub viewport: Size,
    entity: &'a mut EntityContext<'b, T>,
}

impl<'a, 'b, T: 'static> ViewContext<'a, 'b, T> {
    pub fn new(theme: &'a Theme, viewport: Size, entity: &'a mut EntityContext<'b, T>) -> Self {
        Self { theme, viewport, entity }
    }

    /// Acceso al mapa de entidades para la fachada de closures y para trabajo avanzado.
    pub fn entities_mut(&mut self) -> &mut crate::core::EntityMap {
        self.entity
    }

    /// Convierte un cierre sobre el estado de la vista en un manejador de evento:
    /// `.on_click(cx.listener(|this, _event, cx| { this.count += 1; cx.notify(); }))`.
    pub fn listener<E>(
        &self,
        callback: impl Fn(&mut T, &E, &mut ListenerContext<T>) + 'static,
    ) -> impl Fn(&E, &mut EventContext) + 'static {
        let handle = self.entity.handle();
        move |event, context| {
            let effects = &mut context.effects;
            context.entities.update(handle, |this, entity| {
                let mut listener = ListenerContext { entity, effects };
                callback(this, event, &mut listener);
            });
        }
    }

    /// Clic que ejecuta `action` sobre el estado de la vista y la redibuja (se avisa solo, sin `cx.notify()`).
    /// No hace falta nombrar el tipo de la vista ni el evento: `.on_click(cx.click(|this, cx| this.pick(cx)))`.
    pub fn click(
        &self,
        action: impl Fn(&mut T, &mut ListenerContext<T>) + 'static,
    ) -> impl Fn(&ClickEvent, &mut EventContext) + 'static {
        self.listener(move |this: &mut T, _: &ClickEvent, cx| {
            action(this, cx);
            cx.notify();
        })
    }

    /// Clic que invierte un `bool` de la vista: `cx.toggle(|t| &mut t.menu_open)`.
    pub fn toggle(&self, field: fn(&mut T) -> &mut bool) -> impl Fn(&ClickEvent, &mut EventContext) + 'static {
        self.click(move |this, _| {
            let flag = field(this);
            *flag = !*flag;
        })
    }

    /// Clic que asigna un valor a un campo: `cx.set(|t| &mut t.tab, Tab::Audio)`.
    pub fn set<V: Clone + 'static>(
        &self,
        field: fn(&mut T) -> &mut V,
        value: V,
    ) -> impl Fn(&ClickEvent, &mut EventContext) + 'static {
        self.click(move |this, _| *field(this) = value.clone())
    }

    /// Clic que emite un evento de la entidad, para que otra vista o el padre lo escuche con `subscribe`.
    pub fn fire<E: Any + Clone>(&self, event: E) -> impl Fn(&ClickEvent, &mut EventContext) + 'static {
        self.click(move |_, cx| cx.emit(event.clone()))
    }

    /// `true` mientras se arrastra una carga de tipo `T` (pestaña, panel…): para mostrar zonas de soltado.
    pub fn dragging<Payload: std::any::Any>(&self) -> bool {
        self.entity.drag_type == Some(std::any::TypeId::of::<Payload>())
    }

    /// Dibuja otra vista como hija de esta (por ejemplo, un editor dentro de un panel).
    pub fn render_child<V: Component>(&mut self, child: Handle<V>) -> AnyElement {
        let (theme, viewport) = (self.theme, self.viewport);
        self.entity
            .update(child, |view, entity| view.view(&mut ViewContext::new(theme, viewport, entity)).into_element())
            .unwrap_or_else(|| div().into_element())
    }

    /// Progreso (0 a 1) de una animación con nombre; mientras corre pide más frames.
    pub fn animate(&mut self, id: &str, animation: Animation) -> f32 {
        let (progress, running) = self.entity.animations.progress(id, animation, Instant::now());
        if running {
            self.entity.timers.request_animation_frame();
        }
        progress
    }

    /// Interpola entre dos valores (colores, posiciones, tamaños) con el progreso de una animación con nombre.
    pub fn animate_between<L: Lerp>(&mut self, id: &str, animation: Animation, from: L, to: L) -> L {
        from.lerp(to, self.animate(id, animation))
    }

    /// Segundos desde que `id` se usó por primera vez, para animaciones continuas (con [`crate::core::wave`]).
    /// Con `enabled = false` (movimiento reducido o nada que animar) devuelve 0 y no pide más frames.
    pub fn phase(&mut self, id: &str, enabled: bool) -> f32 {
        if !enabled {
            return 0.0;
        }
        let day = Animation::new(Duration::from_secs_f32(PHASE_SECONDS)).easing(Easing::Linear);
        self.animate(id, day) * PHASE_SECONDS
    }

    /// Entrada con fundido: 0 a 1 durante `millis` desde la primera vez que se pinta `id`; con `enabled = false`, 1.
    pub fn fade_in(&mut self, id: &str, millis: u64, enabled: bool) -> f32 {
        if !enabled {
            return 1.0;
        }
        self.animate(id, Animation::new(Duration::from_millis(millis)))
    }

    /// Como `transition` de CSS: el valor se desliza hacia `target` cada vez que este cambia, partiendo de
    /// donde iba. Solo pide frames mientras se mueve.
    pub fn transition(&mut self, id: &str, target: f32, animation: Animation) -> f32 {
        let (value, running) = self.entity.animations.transition(id, target, animation, Instant::now());
        if running {
            self.entity.timers.request_animation_frame();
        }
        value
    }

    pub fn new_focus_handle(&mut self) -> FocusHandle {
        self.entity.focus.new_handle()
    }

    pub fn is_focused(&self, handle: &FocusHandle) -> bool {
        self.entity.focus.is_focused(handle)
    }
}

impl<'b, T> Deref for ViewContext<'_, 'b, T> {
    type Target = EntityContext<'b, T>;

    fn deref(&self) -> &Self::Target {
        self.entity
    }
}

impl<T> DerefMut for ViewContext<'_, '_, T> {
    fn deref_mut(&mut self) -> &mut Self::Target {
        self.entity
    }
}

/// Contexto de un manejador creado con `listener`: acceso a la entidad y a los efectos.
pub struct ListenerContext<'a, 'b, T> {
    entity: &'a mut EntityContext<'b, T>,
    effects: &'a mut Effects,
}

impl<T: 'static> ListenerContext<'_, '_, T> {
    pub fn notify(&mut self) {
        self.effects.notify();
        self.entity.notify();
    }

    pub fn dispatch_action(&mut self, action: Action) {
        self.effects.dispatch_action(action);
    }

    pub fn propagate(&mut self) {
        self.effects.propagate();
    }

    /// Dónde estaba el puntero al soltar un arrastre (solo dentro de un `on_drop`).
    pub fn pointer(&self) -> Option<crate::core::Point> {
        self.effects.pointer
    }

    pub fn focus(&mut self, handle: &FocusHandle) {
        self.entity.focus.focus(handle);
        self.effects.notify();
    }
}

impl<'b, T> Deref for ListenerContext<'_, 'b, T> {
    type Target = EntityContext<'b, T>;

    fn deref(&self) -> &Self::Target {
        self.entity
    }
}

impl<T> DerefMut for ListenerContext<'_, '_, T> {
    fn deref_mut(&mut self) -> &mut Self::Target {
        self.entity
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::core::{EntityMap, MouseButton};

    struct Panel {
        open: bool,
        tab: u8,
        clicks: u32,
    }

    #[derive(Clone)]
    struct Closed;

    type Handler = Box<dyn Fn(&ClickEvent, &mut EventContext)>;

    fn click(entities: &mut EntityMap, handler: &Handler) -> bool {
        let event = ClickEvent {
            position: Default::default(),
            button: MouseButton::Left,
            modifiers: Default::default(),
            click_count: 1,
        };
        let mut context = EventContext { entities, effects: Default::default() };
        handler(&event, &mut context);
        context.effects.redraw
    }

    #[test]
    fn click_toggle_set_and_fire_mutate_redraw_and_emit() {
        let mut entities = EntityMap::new();
        let handle = entities.insert(Panel { open: false, tab: 0, clicks: 0 });
        let seen = std::rc::Rc::new(std::cell::Cell::new(0));
        let counter = seen.clone();
        entities.update(handle, |_, cx| {
            let me = cx.handle();
            cx.subscribe(me, me, move |_: &mut Panel, _: &Closed, _| counter.set(counter.get() + 1));
        });
        let theme = Theme::default();
        let (toggle, set, count, fire) = entities
            .update(handle, |_, entity| {
                let cx = ViewContext::new(&theme, Default::default(), entity);
                let handlers: [Handler; 4] = [
                    Box::new(cx.toggle(|t| &mut t.open)),
                    Box::new(cx.set(|t| &mut t.tab, 3)),
                    Box::new(cx.click(|t, _| t.clicks += 1)),
                    Box::new(cx.fire(Closed)),
                ];
                handlers
            })
            .map(|[a, b, c, d]| (a, b, c, d))
            .unwrap();
        assert!(click(&mut entities, &toggle) && click(&mut entities, &set) && click(&mut entities, &count));
        click(&mut entities, &toggle);
        click(&mut entities, &fire);
        let panel = entities.read(handle).unwrap();
        assert!(!panel.open, "dos toggles vuelven al inicio");
        assert_eq!((panel.tab, panel.clicks), (3, 1));
        assert_eq!(seen.get(), 1, "fire emitió el evento una vez");
    }

    #[test]
    fn phase_and_fade_in_do_not_animate_when_disabled() {
        let mut entities = EntityMap::new();
        let handle = entities.insert(Panel { open: false, tab: 0, clicks: 0 });
        let theme = Theme::default();
        entities.update(handle, |_, entity| {
            let mut cx = ViewContext::new(&theme, Default::default(), entity);
            assert_eq!((cx.phase("a", false), cx.fade_in("b", 200, false)), (0.0, 1.0));
            assert!(cx.fade_in("c", 200, true) < 1.0, "recién empezada, aún no llegó al final");
        });
    }
}