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.
//! Bucle de una aplicación de una ventana. Duerme (`Wait`) salvo que haya un temporizador pendiente
//! (`WaitUntil`), una animación en curso o resultados de otros hilos; tras un rato ocioso devuelve memoria.
//!
//! En móvil sigue el ciclo de vida del sistema: en segundo plano no pinta nada y suelta cachés; Android además
//! destruye la ventana, así que se suelta la GPU y se guarda la UI con su estado hasta volver.

use crate::app::memory::release_freed_pages;
use crate::app::snapshot::SnapshotRequest;
use crate::app::surface::{Surface, SurfaceOptions};
use crate::app::wake::Wake;
use crate::view::window::Window as UiWindow;
use std::time::{Duration, Instant};
use winit::application::ApplicationHandler;
use winit::event::{StartCause, WindowEvent};
use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop, EventLoopProxy};
use winit::window::WindowId;

/// Espera de inactividad antes de devolver páginas libres del asignador.
const RELIEF_DELAY: Duration = Duration::from_secs(3);
/// Frames antes de simular teclas en una instantánea: el primero mide la fuente.
const SNAPSHOT_WARMUP: u32 = 2;

/// Recibe cada archivo que el usuario suelta sobre la ventana.
type DropHandler = Box<dyn FnMut(&mut UiWindow, std::path::PathBuf)>;

pub struct AppOptions {
    pub window: SurfaceOptions,
    on_drop: Option<DropHandler>,
    #[cfg(target_os = "android")]
    android_app: Option<AndroidApp>,
}

#[cfg(target_os = "android")]
pub use winit::platform::android::activity::AndroidApp;

impl AppOptions {
    pub fn new(title: impl Into<String>) -> Self {
        Self {
            window: SurfaceOptions { title: title.into(), ..SurfaceOptions::default() },
            on_drop: None,
            #[cfg(target_os = "android")]
            android_app: None,
        }
    }

    /// La actividad que Android entrega a `android_main`; sin ella no hay ventana en Android.
    #[cfg(target_os = "android")]
    pub fn android_app(mut self, app: AndroidApp) -> Self {
        self.android_app = Some(app);
        self
    }

    pub fn size(mut self, width: f64, height: f64) -> Self {
        self.window.size = (width, height);
        self
    }

    pub fn theme(mut self, theme: crate::view::theme::Theme) -> Self {
        self.window.theme = theme;
        self
    }

    /// Archivos soltados desde el sistema sobre la ventana (uno por llamada).
    pub fn on_file_drop(mut self, handler: impl FnMut(&mut UiWindow, std::path::PathBuf) + 'static) -> Self {
        self.on_drop = Some(Box::new(handler));
        self
    }

    /// Abre la ventana con una closure como raíz: `AppOptions::new("Hola").run_ui(|ui| ...)`.
    pub fn run_ui<F, R>(self, build: F) -> Result<(), Box<dyn std::error::Error>>
    where
        F: FnMut(&mut crate::view::ui::Ui) -> R + 'static,
        R: crate::view::element::IntoElement + 'static,
    {
        run(self, move |window| {
            window.set_root(crate::app::closure::FnApp::new(build));
        })
    }
}

/// Construye la vista raíz (y, si hace falta, el keymap) sobre la ventana recién creada.
type Build = Box<dyn FnOnce(&mut UiWindow)>;

struct Runner {
    proxy: EventLoopProxy<Wake>,
    options: Option<AppOptions>,
    build: Option<Build>,
    on_drop: Option<DropHandler>,
    surface: Option<Surface>,
    /// La UI mientras Android tiene la app en segundo plano y sin ventana.
    parked: Option<UiWindow>,
    snapshot: Option<SnapshotRequest>,
    last_relief: Instant,
    /// Hubo actividad desde la última devolución de memoria; sin ella el bucle no despierta solo para eso.
    relief_due: bool,
}

/// Abre la ventana, llama a `build` para montar la vista raíz y atiende el bucle hasta cerrarla.
///
/// ```no_run
/// use aura_ui::prelude::*;
///
/// AppOptions::new("Hola").run_ui(|ui| ui.page().center().child("Hola")).unwrap();
/// ```
pub fn run(
    #[allow(unused_mut)] mut options: AppOptions,
    build: impl FnOnce(&mut UiWindow) + 'static,
) -> Result<(), Box<dyn std::error::Error>> {
    #[allow(unused_mut)]
    let mut builder = EventLoop::<Wake>::with_user_event();
    #[cfg(target_os = "android")]
    if let Some(app) = options.android_app.take() {
        use winit::platform::android::EventLoopBuilderExtAndroid;
        builder.with_android_app(app);
    }
    #[cfg(target_os = "ios")]
    crate::app::ios_scene::register();
    let event_loop = builder.build()?;
    let mut runner = Runner {
        proxy: event_loop.create_proxy(),
        options: Some(options),
        build: Some(Box::new(build)),
        on_drop: None,
        surface: None,
        parked: None,
        snapshot: SnapshotRequest::from_environment(),
        last_relief: Instant::now(),
        relief_due: false,
    };
    event_loop.run_app(&mut runner)?;
    Ok(())
}

impl ApplicationHandler<Wake> for Runner {
    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
        event_loop.set_control_flow(ControlFlow::Wait);
        if let Some(ui) = self.parked.take() {
            match Surface::reopen(event_loop, &self.proxy, ui) {
                Ok(surface) => {
                    surface.request_redraw();
                    self.surface = Some(surface);
                }
                Err(error) => {
                    crate::app::console::warn(format_args!("no se pudo reabrir la ventana: {error}"));
                    event_loop.exit();
                }
            }
            return;
        }
        if let Some(surface) = self.surface.as_mut() {
            // iOS: vuelve a primer plano con la misma ventana.
            if surface.handle(WindowEvent::Occluded(false)) {
                surface.request_redraw();
            }
            return;
        }
        let (Some(mut options), Some(build)) = (self.options.take(), self.build.take()) else { return };
        self.on_drop = options.on_drop.take();
        options.window.system_clipboard &= self.snapshot.is_none();
        match Surface::create(event_loop, &self.proxy, options.window) {
            Ok(mut surface) => {
                build(&mut surface.ui);
                surface.request_redraw();
                self.surface = Some(surface);
            }
            Err(error) => {
                crate::app::console::warn(format_args!("no se pudo abrir la ventana: {error}"));
                event_loop.exit();
            }
        }
    }

    fn suspended(&mut self, _: &ActiveEventLoop) {
        if cfg!(target_os = "android") {
            self.parked = self.surface.take().map(Surface::into_ui);
        } else if let Some(surface) = self.surface.as_mut() {
            surface.handle(WindowEvent::Occluded(true));
        }
        release_freed_pages();
    }

    /// El sistema (sobre todo iOS y Android) avisa de que falta memoria: se vacían las cachés.
    fn memory_warning(&mut self, _: &ActiveEventLoop) {
        if let Some(surface) = self.surface.as_mut() {
            surface.ui.shrink_caches();
            surface.renderer.shrink_caches();
            surface.request_redraw();
        }
        release_freed_pages();
    }

    fn new_events(&mut self, _: &ActiveEventLoop, cause: StartCause) {
        let (StartCause::ResumeTimeReached { .. }, Some(surface)) = (cause, self.surface.as_mut()) else { return };
        let long_press = surface.touch.tick(Instant::now());
        if surface.dispatch(&long_press) | surface.ui.run_pending() {
            surface.request_redraw();
        }
    }

    fn user_event(&mut self, _: &ActiveEventLoop, event: Wake) {
        let Some(surface) = self.surface.as_mut() else { return };
        self.relief_due = true;
        match event {
            Wake::Access(event) => surface.access_event(event.window_event),
            Wake::Inbox(_) => {
                if surface.ui.run_pending() {
                    surface.request_redraw();
                }
            }
        }
    }

    fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
        let Some(surface) = self.surface.as_mut() else { return };
        if surface.wants_frame() {
            surface.request_redraw();
        }
        if self.relief_due && self.last_relief.elapsed() >= RELIEF_DELAY {
            self.last_relief = Instant::now();
            self.relief_due = false;
            release_freed_pages();
        }
        let relief = self.relief_due.then(|| self.last_relief + RELIEF_DELAY);
        let mut deadline =
            [surface.ui.next_deadline(), surface.touch.next_deadline(), relief].into_iter().flatten().min();
        if let Some(request) = &self.snapshot {
            deadline = deadline.into_iter().chain(Some(request.deadline())).min();
            if request.ready() {
                surface.request_redraw();
            }
        }
        event_loop.set_control_flow(deadline.map_or(ControlFlow::Wait, ControlFlow::WaitUntil));
    }

    fn window_event(&mut self, event_loop: &ActiveEventLoop, _: WindowId, event: WindowEvent) {
        let Some(surface) = self.surface.as_mut() else { return };
        if !matches!(event, WindowEvent::RedrawRequested) || surface.frames_drawn <= 1 {
            self.relief_due = true;
        }
        surface.access.process_event(&surface.window, &event);
        match event {
            WindowEvent::CloseRequested => event_loop.exit(),
            WindowEvent::RedrawRequested => {
                if !surface.visibility.is_drawable() {
                    return;
                }
                if let Err(error) = surface.redraw() {
                    crate::app::console::warn(format_args!("{error}"));
                }
                let Some(request) = &self.snapshot else { return };
                if surface.frames_drawn == SNAPSHOT_WARMUP {
                    for keystroke in &request.keystrokes {
                        surface.press(keystroke);
                        surface.ui.run_pending();
                    }
                }
                if surface.frames_drawn <= SNAPSHOT_WARMUP + 1 {
                    surface.request_redraw();
                } else if request.ready() {
                    surface.save_snapshot(request);
                    event_loop.exit();
                }
            }
            WindowEvent::DroppedFile(path) => {
                if let Some(handler) = self.on_drop.as_mut() {
                    handler(&mut surface.ui, path);
                    surface.ui.run_pending();
                    surface.request_redraw();
                }
            }
            other => {
                if surface.handle(other) {
                    surface.request_redraw();
                }
            }
        }
    }
}