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.
//! Atlas de texturas: empaqueta glifos e iconos con `etagere` en una textura fija de la GPU o, sin GPU, en un
//! búfer en memoria del mismo tamaño (1 MB cada uno). Quien inserta no sabe cuál de los dos hay.

use crate::gpu::text_paint::GlyphTarget;
use etagere::{AllocId, BucketedAtlasAllocator, size2};

/// Región ya subida al atlas, con coordenadas UV normalizadas.
#[derive(Clone, Copy, Debug)]
pub struct AtlasTile {
    pub uv: [f32; 4],
    pub width: u32,
    pub height: u32,
    pub(crate) allocation: AllocId,
}

/// Par de atlas (monocromático para glifos/iconos y policromático para imágenes/emojis) compartido por GPU y CPU.
pub struct AtlasPair {
    pub monochrome: TextureAtlas,
    pub polychrome: TextureAtlas,
}

impl AtlasPair {
    pub fn gpu(device: &wgpu::Device) -> Self {
        Self { monochrome: TextureAtlas::monochrome(device), polychrome: TextureAtlas::polychrome(device) }
    }

    pub fn cpu() -> Self {
        Self { monochrome: TextureAtlas::cpu_monochrome(), polychrome: TextureAtlas::cpu_polychrome() }
    }

    pub fn target<'a>(&'a mut self, queue: Option<&'a wgpu::Queue>) -> GlyphTarget<'a> {
        GlyphTarget { queue, monochrome_atlas: &mut self.monochrome, polychrome_atlas: &mut self.polychrome }
    }

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

    pub fn clear(&mut self) {
        self.monochrome.clear();
        self.polychrome.clear();
    }
}

/// Dónde viven los píxeles del atlas.
enum Storage {
    Gpu {
        texture: wgpu::Texture,
        view: wgpu::TextureView,
    },
    /// Filas de `size` píxeles de `bytes_per_pixel` bytes, para el pintado por CPU.
    Cpu(Vec<u8>),
}

pub struct TextureAtlas {
    storage: Storage,
    allocator: BucketedAtlasAllocator,
    format: wgpu::TextureFormat,
    size: u32,
    /// Una inserción no cupo desde el último `take_overflow`: hay glifos o iconos sin pintar.
    overflowed: bool,
}

/// Margen de 1 px alrededor de cada tile para evitar sangrado al filtrar.
const TILE_PADDING: u32 = 1;

impl TextureAtlas {
    /// `R8Unorm` de 1024x1024 ocupa solo 1 MB de VRAM.
    pub fn monochrome(device: &wgpu::Device) -> Self {
        Self::new(device, wgpu::TextureFormat::R8Unorm, 1024, "aura_atlas_mono")
    }

    /// El atlas de color también aloja capturas adjuntas (hasta 1024 px por lado).
    /// Debe dejar sitio para su margen y para varias miniaturas a la vez.
    pub fn polychrome(device: &wgpu::Device) -> Self {
        Self::new(device, wgpu::TextureFormat::Rgba8Unorm, 2048, "aura_atlas_color")
    }

    fn new(device: &wgpu::Device, format: wgpu::TextureFormat, size: u32, label: &str) -> Self {
        let texture = device.create_texture(&wgpu::TextureDescriptor {
            label: Some(label),
            size: wgpu::Extent3d { width: size, height: size, depth_or_array_layers: 1 },
            mip_level_count: 1,
            sample_count: 1,
            dimension: wgpu::TextureDimension::D2,
            format,
            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
            view_formats: &[],
        });
        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
        Self::with_storage(Storage::Gpu { texture, view }, format, size)
    }

    /// Atlas en memoria con el mismo formato y tamaño que su par de GPU, para pintar sin GPU.
    pub fn cpu_monochrome() -> Self {
        Self::with_storage(Storage::Cpu(vec![0; 1024 * 1024]), wgpu::TextureFormat::R8Unorm, 1024)
    }

    pub fn cpu_polychrome() -> Self {
        Self::with_storage(Storage::Cpu(vec![0; 2048 * 2048 * 4]), wgpu::TextureFormat::Rgba8Unorm, 2048)
    }

    fn with_storage(storage: Storage, format: wgpu::TextureFormat, size: u32) -> Self {
        let allocator = BucketedAtlasAllocator::new(size2(size as i32, size as i32));
        Self { storage, allocator, format, size, overflowed: false }
    }

    /// Vista de la textura; solo existe en un atlas de GPU.
    pub fn view(&self) -> &wgpu::TextureView {
        match &self.storage {
            Storage::Gpu { view, .. } => view,
            Storage::Cpu(_) => panic!("un atlas en memoria no tiene vista de GPU"),
        }
    }

    /// `(píxeles, lado, bytes por píxel)` de un atlas en memoria.
    pub fn cpu_pixels(&self) -> Option<(&[u8], u32, u32)> {
        match &self.storage {
            Storage::Cpu(pixels) => Some((pixels, self.size, self.bytes_per_pixel())),
            Storage::Gpu { .. } => None,
        }
    }

    fn bytes_per_pixel(&self) -> u32 {
        match self.format {
            wgpu::TextureFormat::R8Unorm => 1,
            _ => 4,
        }
    }

    /// Reserva espacio y copia los píxeles (a la GPU con `queue`, o a memoria). `None` si el atlas está lleno.
    pub fn insert(&mut self, queue: Option<&wgpu::Queue>, width: u32, height: u32, pixels: &[u8]) -> Option<AtlasTile> {
        let padded = size2((width + TILE_PADDING * 2) as i32, (height + TILE_PADDING * 2) as i32);
        let Some(allocation) = self.allocator.allocate(padded) else {
            self.overflowed = true;
            return None;
        };
        let x = allocation.rectangle.min.x as u32 + TILE_PADDING;
        let y = allocation.rectangle.min.y as u32 + TILE_PADDING;

        let bytes = self.bytes_per_pixel();
        match (&mut self.storage, queue) {
            (Storage::Gpu { texture, .. }, Some(queue)) => queue.write_texture(
                wgpu::TexelCopyTextureInfo {
                    texture,
                    mip_level: 0,
                    origin: wgpu::Origin3d { x, y, z: 0 },
                    aspect: wgpu::TextureAspect::All,
                },
                pixels,
                wgpu::TexelCopyBufferLayout {
                    offset: 0,
                    bytes_per_row: Some(width * bytes),
                    rows_per_image: Some(height),
                },
                wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
            ),
            (Storage::Cpu(storage), _) => {
                let row = (width * bytes) as usize;
                for (line, source) in pixels.chunks(row).take(height as usize).enumerate() {
                    let start = (((y as usize + line) * self.size as usize) + x as usize) * bytes as usize;
                    storage[start..start + source.len()].copy_from_slice(source);
                }
            }
            (Storage::Gpu { .. }, None) => return None,
        }

        let scale = 1.0 / self.size as f32;
        Some(AtlasTile {
            uv: [x as f32 * scale, y as f32 * scale, (x + width) as f32 * scale, (y + height) as f32 * scale],
            width,
            height,
            allocation: allocation.id,
        })
    }

    /// `true` (una sola vez) si alguna inserción no cupo.
    pub fn take_overflow(&mut self) -> bool {
        std::mem::take(&mut self.overflowed)
    }

    /// Libera un tile que ya no se pintará (una imagen sustituida por otra), sin vaciar el resto.
    pub fn remove(&mut self, tile: &AtlasTile) {
        self.allocator.deallocate(tile.allocation);
    }

    /// Descarta todos los tiles; quien los cacheaba debe invalidar sus entradas.
    pub fn clear(&mut self) {
        self.allocator.clear();
    }
}