aura-gui 0.2.2

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.
//! Pintado sin GPU para equipos viejos o sin controladores: la misma `Scene` que usa la GPU, rasterizada por la CPU
//! y presentada con softbuffer (GDI en Windows, CoreGraphics en macOS, X11/Wayland en Linux).
//!
//! Memoria: se pinta directamente en el búfer de la ventana (no hay una segunda copia del frame) y los atlas de
//! glifos e imágenes ocupan 1 MB cada uno, igual que en la GPU.

pub mod blur;
pub mod canvas;
mod present;
pub mod shapes;
pub mod sprites;

use crate::core::Rgba;
use crate::gpu::atlas::TextureAtlas;
use crate::gpu::capture::CapturedFrame;
use crate::gpu::context::Error;
use crate::gpu::scene::{PrimitiveKind, Scene};
use crate::gpu::text::GlyphTarget;
use canvas::Canvas;
pub use present::{Presenter, SoftbufferPresenter};
use sprites::AtlasView;

fn view(atlas: &TextureAtlas) -> Option<AtlasView<'_>> {
    atlas.cpu_pixels().map(|(pixels, size, channels)| AtlasView { pixels, size, channels })
}

pub struct CpuRenderer {
    presenter: Option<Box<dyn Presenter>>,
    width: u32,
    height: u32,
    monochrome_atlas: TextureAtlas,
    polychrome_atlas: TextureAtlas,
}

impl CpuRenderer {
    /// Sin `presenter` solo sirve para capturar (pruebas sin ventana).
    pub fn new(presenter: Option<Box<dyn Presenter>>, width: u32, height: u32) -> Self {
        Self {
            presenter,
            width: width.max(1),
            height: height.max(1),
            monochrome_atlas: TextureAtlas::cpu_monochrome(),
            polychrome_atlas: TextureAtlas::cpu_polychrome(),
        }
    }

    pub fn size(&self) -> (u32, u32) {
        (self.width, self.height)
    }

    pub fn resize(&mut self, width: u32, height: u32) {
        if width > 0 && height > 0 {
            (self.width, self.height) = (width, height);
        }
    }

    pub fn glyph_target(&mut self) -> GlyphTarget<'_> {
        GlyphTarget {
            queue: None,
            monochrome_atlas: &mut self.monochrome_atlas,
            polychrome_atlas: &mut self.polychrome_atlas,
        }
    }

    pub fn take_atlas_overflow(&mut self) -> bool {
        let mono = self.monochrome_atlas.take_overflow();
        let color = self.polychrome_atlas.take_overflow();
        mono || color
    }

    pub fn shrink_caches(&mut self) {
        self.monochrome_atlas.clear();
        self.polychrome_atlas.clear();
    }

    /// Pinta la escena en `pixels` (`0x00RRGGBB`, `width × height`), lote a lote en el mismo orden que la GPU.
    pub fn draw(&self, pixels: &mut [u32], scene: &Scene, scale: f32, clear: Rgba) {
        let mut canvas = Canvas { pixels, width: self.width, height: self.height, scale };
        canvas.clear(clear);
        let (mono, color) = (view(&self.monochrome_atlas), view(&self.polychrome_atlas));
        for batch in &scene.batches {
            let range = batch.range.start as usize..batch.range.end as usize;
            // Sin giro la transformación solo mueve y escala rectángulos y se aplica aquí; un giro o una inclinación
            // necesitan el shader de la GPU y, sin ella, ese lote se pinta sin transformar.
            let transform = scene.transforms.get(batch.transform as usize).copied().unwrap_or_default();
            let moved = !transform.is_identity() && transform.is_axis_aligned();
            match batch.kind {
                PrimitiveKind::Blur => scene.blurs[range].iter().for_each(|b| blur::blur(&mut canvas, b)),
                PrimitiveKind::Shadow => scene.shadows[range].iter().for_each(|s| match moved {
                    true => shapes::shadow(&mut canvas, &transform.shadow(s)),
                    false => shapes::shadow(&mut canvas, s),
                }),
                PrimitiveKind::Quad => scene.quads[range].iter().for_each(|q| match moved {
                    true => shapes::quad(&mut canvas, &transform.quad(q)),
                    false => shapes::quad(&mut canvas, q),
                }),
                PrimitiveKind::Underline => scene.underlines[range].iter().for_each(|u| match moved {
                    true => shapes::underline(&mut canvas, &transform.underline(u)),
                    false => shapes::underline(&mut canvas, u),
                }),
                PrimitiveKind::Monochrome => {
                    let Some(atlas) = &mono else { continue };
                    for sprite in &scene.monochrome_sprites[range] {
                        let sprite = if moved { transform.sprite(sprite) } else { *sprite };
                        sprites::sprite(&mut canvas, atlas, &sprite, false);
                    }
                }
                PrimitiveKind::Polychrome => {
                    let Some(atlas) = &color else { continue };
                    for sprite in &scene.polychrome_sprites[range] {
                        let sprite = if moved { transform.sprite(sprite) } else { *sprite };
                        sprites::sprite(&mut canvas, atlas, &sprite, true);
                    }
                }
            }
        }
    }

    /// `Ok(false)` si no hay dónde presentar.
    pub fn render(&mut self, scene: &Scene, scale: f32, clear: Rgba) -> Result<bool, Error> {
        let Some(mut presenter) = self.presenter.take() else { return Ok(false) };
        let (width, height) = (self.width, self.height);
        let result = presenter.present(width, height, &mut |pixels| self.draw(pixels, scene, scale, clear));
        self.presenter = Some(presenter);
        result.map(|()| true)
    }

    pub fn capture(&mut self, scene: &Scene, scale: f32, clear: Rgba) -> CapturedFrame {
        let mut pixels = vec![0u32; (self.width * self.height) as usize];
        self.draw(&mut pixels, scene, scale, clear);
        let rgba =
            pixels.iter().flat_map(|pixel| [(pixel >> 16) as u8, (pixel >> 8) as u8, *pixel as u8, 255]).collect();
        CapturedFrame { width: self.width, height: self.height, rgba }
    }
}