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.
//! Pintado de texto: rasteriza glifos al atlas y emite sprites y subrayados.

use crate::core::{Bounds, Corners, Pixels, Point, Rgba, Size};
use crate::gpu::atlas::{AtlasTile, TextureAtlas};
use crate::gpu::scene::{MonochromeSprite, Quad, Scene, Underline};
use crate::gpu::text::{CODE_FLAG, STRIKE_FLAG, ShapedText, TextSystem, UNDERLINE_FLAG};
use crate::gpu::text_crisp;
use cosmic_text::{CacheKey, SwashContent};

#[derive(Clone, Copy)]
pub(crate) struct GlyphTile {
    tile: AtlasTile,
    left: i32,
    top: i32,
    is_color: bool,
}

/// Dónde y cómo pintar un texto ya maquetado.
#[derive(Clone, Copy)]
pub struct TextPlacement {
    pub origin: Point,
    /// Color por defecto; los tramos con color propio lo ignoran.
    pub color: Rgba,
    pub clip: Bounds,
    /// Factor de escala de la ventana.
    pub scale: f32,
}

/// Atlas y cola necesarios para subir glifos mientras se pinta.
pub struct GlyphTarget<'a> {
    /// `None` cuando se pinta por CPU: los atlas guardan sus píxeles en memoria.
    pub queue: Option<&'a wgpu::Queue>,
    pub monochrome_atlas: &'a mut TextureAtlas,
    pub polychrome_atlas: &'a mut TextureAtlas,
}

fn glyph_color(color: Option<cosmic_text::Color>, fallback: Rgba) -> Rgba {
    match color {
        Some(color) => {
            let [red, green, blue, alpha] = color.as_rgba();
            Rgba::new(red as f32 / 255.0, green as f32 / 255.0, blue as f32 / 255.0, alpha as f32 / 255.0 * fallback.a)
        }
        None => fallback,
    }
}

impl TextSystem {
    /// Emite un sprite por glifo y un subrayado por cada tramo marcado.
    pub fn paint(
        &mut self,
        shaped: &ShapedText,
        placement: &TextPlacement,
        target: &mut GlyphTarget,
        scene: &mut Scene,
    ) {
        let TextPlacement { origin, color, clip, scale } = *placement;
        for run in shaped.buffer.layout_runs() {
            paint_code_chips(&run, placement, scene);
            let offset = (origin.x.0 * scale, (origin.y.0 + run.line_y) * scale);
            for glyph in run.glyphs {
                let tint = glyph_color(glyph.color_opt, color);
                if glyph.metadata & UNDERLINE_FLAG != 0 {
                    let underline = Bounds::new(
                        Point::new(Pixels(origin.x.0 + glyph.x), Pixels(origin.y.0 + run.line_y + 1.0)),
                        Size::new(Pixels(glyph.w), Pixels(2.0)),
                    );
                    scene.insert_underline(Underline::new(underline, clip, tint, 1.0, false));
                }
                if glyph.metadata & STRIKE_FLAG != 0 {
                    let middle = origin.y.0 + run.line_y - glyph.font_size * 0.3;
                    let strike = Bounds::new(
                        Point::new(Pixels(origin.x.0 + glyph.x), Pixels(middle)),
                        Size::new(Pixels(glyph.w), Pixels(1.0)),
                    );
                    scene.insert_quad(Quad::new(strike, clip, tint));
                }
                let mut physical = glyph.physical(offset, scale);
                let crisp = text_crisp::is_crisp(scale);
                if crisp {
                    physical.x += text_crisp::snap_x(&mut physical.cache_key.x_bin);
                }
                let Some(entry) = self.glyph_tile(physical.cache_key, crisp, target) else { continue };
                let bounds = Bounds::new(
                    Point::new(
                        Pixels((physical.x + entry.left) as f32 / scale),
                        Pixels((physical.y - entry.top) as f32 / scale),
                    ),
                    Size::new(Pixels(entry.tile.width as f32 / scale), Pixels(entry.tile.height as f32 / scale)),
                );
                if entry.is_color {
                    // Los emojis llevan su propio color; solo se modula la opacidad.
                    let sprite = MonochromeSprite::new(bounds, clip, entry.tile.uv, Rgba::new(1.0, 1.0, 1.0, tint.a));
                    scene.insert_polychrome(sprite);
                } else {
                    scene.insert_monochrome(MonochromeSprite::new(bounds, clip, entry.tile.uv, tint));
                }
            }
        }
    }

    fn glyph_tile(&mut self, key: CacheKey, crisp: bool, target: &mut GlyphTarget) -> Option<GlyphTile> {
        if let Some(cached) = self.tiles.get(&(key, crisp)) {
            return *cached;
        }
        let image = self.swash_cache.get_image_uncached(&mut self.font_system, key);
        let entry = image.and_then(|image| {
            let (width, height) = (image.placement.width, image.placement.height);
            if width == 0 || height == 0 {
                return None;
            }
            let (tile, is_color) = match image.content {
                SwashContent::Mask => {
                    let mut mask = image.data;
                    if crisp {
                        text_crisp::boost(&mut mask);
                    }
                    (target.monochrome_atlas.insert(target.queue, width, height, &mask)?, false)
                }
                SwashContent::Color => {
                    let premultiplied = premultiply_rgba(&image.data);
                    (target.polychrome_atlas.insert(target.queue, width, height, &premultiplied)?, true)
                }
                SwashContent::SubpixelMask => return None,
            };
            Some(GlyphTile { tile, left: image.placement.left, top: image.placement.top, is_color })
        });
        self.tiles.insert((key, crisp), entry);
        entry
    }
}

/// Fondo de los tramos de código de una línea: uno por tramo seguido (no uno por glifo), antes que los glifos
/// para que queden detrás. El color es el del texto, muy transparente: sirve en cualquier tema.
fn paint_code_chips(run: &cosmic_text::LayoutRun, placement: &TextPlacement, scene: &mut Scene) {
    const PAD: f32 = 2.0;
    let mut chip: Option<(f32, f32)> = None;
    let flush = |chip: Option<(f32, f32)>, scene: &mut Scene| {
        let Some((start, end)) = chip else { return };
        let bounds = Bounds::new(
            Point::new(Pixels(placement.origin.x.0 + start - PAD), Pixels(placement.origin.y.0 + run.line_top + 1.0)),
            Size::new(Pixels(end - start + 2.0 * PAD), Pixels((run.line_height - 2.0).max(1.0))),
        );
        let quad =
            Quad::new(bounds, placement.clip, placement.color.with_alpha(0.1)).corners(Corners::all(Pixels(3.0)));
        scene.insert_quad(quad);
    };
    for glyph in run.glyphs {
        if glyph.metadata & CODE_FLAG == 0 {
            flush(chip.take(), scene);
            continue;
        }
        chip = Some(chip.map_or((glyph.x, glyph.x + glyph.w), |(start, _)| (start, glyph.x + glyph.w)));
    }
    flush(chip, scene);
}

pub(crate) fn premultiply_rgba(straight: &[u8]) -> Vec<u8> {
    straight
        .as_chunks::<4>()
        .0
        .iter()
        .flat_map(|pixel| {
            let alpha = pixel[3] as u16;
            [
                ((pixel[0] as u16 * alpha) / 255) as u8,
                ((pixel[1] as u16 * alpha) / 255) as u8,
                ((pixel[2] as u16 * alpha) / 255) as u8,
                pixel[3],
            ]
        })
        .collect()
}