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.
// Uber-shader SDF de Aura. Todas las primitivas son instancias de un quad
// unitario (triangle strip de 4 vértices) leídas desde storage buffers.

// Una por lote, con desplazamiento dinámico. `matrix` = (a, b, c, d) y `offset` = (tx, ty, factor de escala, 0) de
// la transformación afín del lote; sin transformación es la identidad.
struct Globals {
    viewport: vec2<f32>,
    padding: vec2<f32>,
    matrix: vec4<f32>,
    offset: vec4<f32>,
}

@group(0) @binding(0) var<uniform> globals: Globals;

struct Quad {
    bounds: vec4<f32>,
    clip: vec4<f32>,
    background: vec4<f32>,
    border_color: vec4<f32>,
    corner_radii: vec4<f32>,
    border_widths: vec4<f32>,
    gradient_end: vec4<f32>,
    gradient: vec4<f32>,
}

struct Shadow {
    bounds: vec4<f32>,
    clip: vec4<f32>,
    color: vec4<f32>,
    corner_radii: vec4<f32>,
    blur: f32,
}

struct Underline {
    bounds: vec4<f32>,
    clip: vec4<f32>,
    color: vec4<f32>,
    thickness: f32,
    wavy: f32,
}

struct Blur {
    bounds: vec4<f32>,
    clip: vec4<f32>,
    corner_radii: vec4<f32>,
    sigma: f32,
}

struct Sprite {
    bounds: vec4<f32>,
    clip: vec4<f32>,
    uv: vec4<f32>,
    color: vec4<f32>,
}

@group(1) @binding(0) var<storage, read> quads: array<Quad>;
@group(1) @binding(0) var<storage, read> shadows: array<Shadow>;
@group(1) @binding(0) var<storage, read> underlines: array<Underline>;
@group(1) @binding(0) var<storage, read> sprites: array<Sprite>;
@group(1) @binding(0) var<storage, read> blurs: array<Blur>;
@group(2) @binding(0) var atlas_texture: texture_2d<f32>;
@group(2) @binding(1) var atlas_sampler: sampler;

struct VertexOutput {
    @builtin(position) position: vec4<f32>,
    @location(0) logical: vec2<f32>,
    @location(1) @interpolate(flat) instance: u32,
    @location(2) uv: vec2<f32>,
}

fn quad_vertex(index: u32, bounds: vec4<f32>, instance: u32, padding: f32) -> VertexOutput {
    let corner = vec2<f32>(f32(index & 1u), f32(index >> 1u));
    let origin = bounds.xy - vec2<f32>(padding);
    let extent = bounds.zw + vec2<f32>(padding * 2.0);
    let logical = origin + corner * extent;
    // `logical` queda en el espacio local (para el SDF y el recorte); solo la posición en pantalla se transforma.
    let screen = vec2<f32>(
        globals.matrix.x * logical.x + globals.matrix.z * logical.y + globals.offset.x,
        globals.matrix.y * logical.x + globals.matrix.w * logical.y + globals.offset.y,
    );
    let ndc = screen / globals.viewport * vec2<f32>(2.0, -2.0) + vec2<f32>(-1.0, 1.0);
    var out: VertexOutput;
    out.position = vec4<f32>(ndc, 0.0, 1.0);
    out.logical = logical;
    out.instance = instance;
    out.uv = corner;
    return out;
}

// 1 dentro del recorte (ContentMask), 0 fuera.
fn clip_alpha(position: vec2<f32>, clip: vec4<f32>) -> f32 {
    let inside = step(clip.xy, position) * step(position, clip.xy + clip.zw);
    return inside.x * inside.y;
}

// Distancia con signo a un rectángulo redondeado; radios en orden tl, tr, br, bl.
fn rounded_rect_distance(point: vec2<f32>, half_size: vec2<f32>, radii: vec4<f32>) -> f32 {
    let side = select(radii.xw, radii.yz, point.x > 0.0);
    let radius = select(side.x, side.y, point.y > 0.0);
    let q = abs(point) - half_size + vec2<f32>(radius);
    return min(max(q.x, q.y), 0.0) + length(max(q, vec2<f32>(0.0))) - radius;
}

fn premultiply(color: vec4<f32>) -> vec4<f32> {
    return vec4<f32>(color.rgb * color.a, color.a);
}

// ---------- Quads ----------

@vertex
fn vs_quad(@builtin(vertex_index) vertex: u32, @builtin(instance_index) instance: u32) -> VertexOutput {
    return quad_vertex(vertex, quads[instance].bounds, instance, 1.0);
}

@fragment
fn fs_quad(in: VertexOutput) -> @location(0) vec4<f32> {
    let quad = quads[in.instance];
    let half_size = quad.bounds.zw * 0.5;
    let center = quad.bounds.xy + half_size;
    // Las distancias son locales: con la escala del lote el borde sigue medio píxel de suave en pantalla.
    let outer = rounded_rect_distance(in.logical - center, half_size, quad.corner_radii) * globals.offset.z;
    let coverage = clamp(0.5 - outer, 0.0, 1.0) * clip_alpha(in.logical, quad.clip);

    // widths: top, right, bottom, left
    let widths = quad.border_widths;
    let inner_half = half_size - vec2<f32>(widths.w + widths.y, widths.x + widths.z) * 0.5;
    let inner_center = center + vec2<f32>(widths.w - widths.y, widths.x - widths.z) * 0.5;
    let inner_radii = max(quad.corner_radii - vec4<f32>(max(max(widths.x, widths.y), max(widths.z, widths.w))), vec4<f32>(0.0));
    let inner = rounded_rect_distance(in.logical - inner_center, inner_half, inner_radii) * globals.offset.z;
    let has_border = step(0.001, widths.x + widths.y + widths.z + widths.w);
    let border_mix = clamp(0.5 + inner, 0.0, 1.0) * has_border;

    var fill = premultiply(quad.background);
    if quad.gradient.z > 0.5 {
        // Largo del degradado como en CSS: la proyección de la caja sobre la dirección.
        let direction = quad.gradient.xy;
        let length = abs(quad.bounds.z * direction.x) + abs(quad.bounds.w * direction.y);
        let t = clamp(0.5 + dot(in.logical - center, direction) / max(length, 0.001), 0.0, 1.0);
        fill = mix(fill, premultiply(quad.gradient_end), t);
    }
    let border = premultiply(quad.border_color);
    return mix(fill, border, border_mix) * coverage;
}

// ---------- Sombras ----------

@vertex
fn vs_shadow(@builtin(vertex_index) vertex: u32, @builtin(instance_index) instance: u32) -> VertexOutput {
    return quad_vertex(vertex, shadows[instance].bounds, instance, shadows[instance].blur * 3.0);
}

// Aproximación de erf (Abramowitz-Stegun) para la integral gaussiana.
fn erf_approx(value: f32) -> f32 {
    let x = abs(value);
    let t = 1.0 / (1.0 + 0.3275911 * x);
    let poly = t * (0.254829592 + t * (-0.284496736 + t * (1.421413741 + t * (-1.453152027 + t * 1.061405429))));
    return sign(value) * (1.0 - poly * exp(-x * x));
}

@fragment
fn fs_shadow(in: VertexOutput) -> @location(0) vec4<f32> {
    let shadow = shadows[in.instance];
    let half_size = shadow.bounds.zw * 0.5;
    let center = shadow.bounds.xy + half_size;
    let distance = rounded_rect_distance(in.logical - center, half_size, shadow.corner_radii);
    let sigma = max(shadow.blur * 0.5, 0.001);
    let falloff = 0.5 * (1.0 - erf_approx(distance / (sigma * 1.41421356)));
    let alpha = shadow.color.a * falloff * clip_alpha(in.logical, shadow.clip);
    return vec4<f32>(shadow.color.rgb * alpha, alpha);
}

// ---------- Subrayados ----------

@vertex
fn vs_underline(@builtin(vertex_index) vertex: u32, @builtin(instance_index) instance: u32) -> VertexOutput {
    return quad_vertex(vertex, underlines[instance].bounds, instance, 0.0);
}

@fragment
fn fs_underline(in: VertexOutput) -> @location(0) vec4<f32> {
    let underline = underlines[in.instance];
    let local = in.logical - underline.bounds.xy;
    let center_y = underline.bounds.w * 0.5;
    let wave = sin(local.x * 1.2) * underline.bounds.w * 0.25 * underline.wavy;
    let distance = (abs(local.y - center_y - wave) - underline.thickness * 0.5) * globals.offset.z;
    let alpha = clamp(0.5 - distance, 0.0, 1.0) * underline.color.a * clip_alpha(in.logical, underline.clip);
    return vec4<f32>(underline.color.rgb * alpha, alpha);
}

// ---------- Sprites (glifos monocromáticos e imágenes a color) ----------

@vertex
fn vs_sprite(@builtin(vertex_index) vertex: u32, @builtin(instance_index) instance: u32) -> VertexOutput {
    var out = quad_vertex(vertex, sprites[instance].bounds, instance, 0.0);
    let sprite = sprites[instance];
    out.uv = mix(sprite.uv.xy, sprite.uv.zw, out.uv);
    return out;
}

@fragment
fn fs_monochrome(in: VertexOutput) -> @location(0) vec4<f32> {
    let sprite = sprites[in.instance];
    let mask = textureSample(atlas_texture, atlas_sampler, in.uv).r;
    let alpha = mask * sprite.color.a * clip_alpha(in.logical, sprite.clip);
    return vec4<f32>(sprite.color.rgb * alpha, alpha);
}

@fragment
fn fs_polychrome(in: VertexOutput) -> @location(0) vec4<f32> {
    let sprite = sprites[in.instance];
    let texel = textureSample(atlas_texture, atlas_sampler, in.uv);
    return texel * sprite.color.a * clip_alpha(in.logical, sprite.clip);
}

// ---------- Desenfoque: pega la textura ya difuminada dentro de un rectángulo redondeado ----------

@vertex
fn vs_blur(@builtin(vertex_index) vertex: u32, @builtin(instance_index) instance: u32) -> VertexOutput {
    return quad_vertex(vertex, blurs[instance].bounds, instance, 1.0);
}

@fragment
fn fs_blur(in: VertexOutput) -> @location(0) vec4<f32> {
    let blur = blurs[in.instance];
    let half_size = blur.bounds.zw * 0.5;
    let center = blur.bounds.xy + half_size;
    let distance = rounded_rect_distance(in.logical - center, half_size, blur.corner_radii);
    let coverage = clamp(0.5 - distance, 0.0, 1.0) * clip_alpha(in.logical, blur.clip);
    // `logical` ya está en px lógicos y la textura difuminada cubre toda la ventana.
    let color = textureSampleLevel(atlas_texture, atlas_sampler, in.logical / globals.viewport, 0.0);
    return vec4<f32>(color.rgb * coverage, coverage);
}