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.
//! Arnés de pruebas sin GPU, como `TestAppContext` de GPUI: simula teclas y ratón,
//! dibuja y ejecuta el trabajo pendiente hasta que la ventana queda en reposo.

use crate::core::{Handle, InputEvent, MouseButton, Point, Size, px};
use crate::view::render::Component;
use crate::view::theme::Theme;
use crate::view::window::Window;
use std::time::{Duration, Instant};

/// Sin trabajo nuevo durante este lapso, la ventana se considera en reposo.
const QUIET_PERIOD: Duration = Duration::from_millis(30);
const PARK_TIMEOUT: Duration = Duration::from_secs(10);

pub struct TestWindow {
    pub window: Window,
}

impl TestWindow {
    /// Ventana de 800×600 con el tema oscuro y `view` como raíz, ya dibujada.
    pub fn new<V: Component>(view: impl FnOnce(&mut Window) -> V) -> (Self, Handle<V>) {
        let mut window = Window::new(Theme::dark(), Size::new(px(800.0), px(600.0)), 1.0);
        let root = view(&mut window);
        let handle = window.set_root(root);
        window.draw(None);
        (Self { window }, handle)
    }

    fn event(&mut self, event: InputEvent) {
        self.window.handle_event(&event);
        self.window.draw(None);
    }

    /// Teclas separadas por espacios (`"mod-p a b enter"`). Un carácter suelto, sin
    /// modificadores, también se escribe como texto.
    pub fn simulate_keystrokes(&mut self, keystrokes: &str) {
        for keystroke in keystrokes.split_whitespace() {
            let mut chars = keystroke.chars();
            let text = match (chars.next(), chars.next()) {
                (Some(character), None) => Some(character.to_string()),
                _ if keystroke == "space" => Some(" ".to_owned()),
                _ => None,
            };
            self.event(InputEvent::KeyDown { keystroke: keystroke.to_owned(), text });
        }
    }

    pub fn simulate_mouse_move(&mut self, x: f32, y: f32) {
        self.event(InputEvent::MouseMove { position: Point::new(px(x), px(y)) });
    }

    pub fn simulate_mouse_down(&mut self, x: f32, y: f32) {
        self.event(InputEvent::MouseDown { position: Point::new(px(x), px(y)), button: MouseButton::Left });
    }

    pub fn simulate_mouse_up(&mut self, x: f32, y: f32) {
        self.event(InputEvent::MouseUp { position: Point::new(px(x), px(y)), button: MouseButton::Left });
    }

    pub fn simulate_click(&mut self, x: f32, y: f32) {
        self.simulate_mouse_move(x, y);
        self.simulate_mouse_down(x, y);
        self.simulate_mouse_up(x, y);
    }

    /// Arrastra con el botón izquierdo de `from` a `to`, pasando por un punto intermedio.
    pub fn simulate_drag(&mut self, from: (f32, f32), to: (f32, f32)) {
        self.simulate_mouse_move(from.0, from.1);
        self.simulate_mouse_down(from.0, from.1);
        self.simulate_mouse_move((from.0 + to.0) / 2.0, (from.1 + to.1) / 2.0);
        self.simulate_mouse_move(to.0, to.1);
        self.simulate_mouse_up(to.0, to.1);
    }

    /// Ejecuta temporizadores vencidos y resultados de hilos hasta que no llegue nada
    /// durante `QUIET_PERIOD`. Falla si la ventana nunca queda en reposo.
    pub fn run_until_parked(&mut self) {
        let deadline = Instant::now() + PARK_TIMEOUT;
        let mut last_work = Instant::now();
        while last_work.elapsed() < QUIET_PERIOD {
            assert!(Instant::now() < deadline, "la ventana nunca quedó en reposo");
            // `|` y no `||`: hay que consumir siempre la petición de frame de animación.
            if self.window.run_pending() | self.window.wants_animation_frame() {
                self.window.draw(None);
                last_work = Instant::now();
            }
            std::thread::sleep(Duration::from_millis(1));
        }
    }

    pub fn read<T: 'static, R>(&self, handle: Handle<T>, read: impl FnOnce(&T) -> R) -> R {
        read(self.window.entities.read(handle).expect("la entidad existe"))
    }
}