lgui-core 0.2.2

Platform-neutral runtime and UI primitives for LGUI
Documentation
use std::collections::VecDeque;

use crate::core::ShadowStyle;

/// Composites an alpha-derived shadow behind a padded premultiplied BGRA surface.
/// All backends share this filter so spread, blur and fractional offsets agree.
pub(crate) fn composite_shadow(pixels: &mut [u8], width: usize, height: usize, style: ShadowStyle) {
    if width == 0 || height == 0 || style.alpha == 0 {
        return;
    }
    assert_eq!(pixels.len(), width * height * 4);
    let mut mask: Vec<u8> = pixels.chunks_exact(4).map(|pixel| pixel[3]).collect();
    let radius = style.spread_radius().abs().ceil() as usize;
    if radius > 0 {
        mask = spread_mask(&mask, width, height, radius, style.spread_radius() > 0.0);
    }
    if style.blur_sigma() > 0.0 {
        let image = image::GrayImage::from_raw(width as u32, height as u32, mask)
            .expect("alpha dimensions");
        mask = image::imageops::blur(&image, style.blur_sigma()).into_raw();
    }
    let color = [
        style.color.0 & 255,
        (style.color.0 >> 8) & 255,
        (style.color.0 >> 16) & 255,
    ];
    for y in 0..height {
        for x in 0..width {
            let alpha = sample(
                &mask,
                width,
                height,
                x as f32 - style.offset_x(),
                y as f32 - style.offset_y(),
            );
            let shadow_alpha = (alpha * style.alpha as f32 / 255.0).round() as u32;
            let pixel = &mut pixels[(y * width + x) * 4..][..4];
            let behind = (shadow_alpha * (255 - pixel[3] as u32) + 127) / 255;
            for channel in 0..3 {
                pixel[channel] =
                    (pixel[channel] as u32 + (color[channel] * behind + 127) / 255).min(255) as u8;
            }
            pixel[3] = (pixel[3] as u32 + behind).min(255) as u8;
        }
    }
}

fn sample(mask: &[u8], width: usize, height: usize, x: f32, y: f32) -> f32 {
    let left = x.floor() as isize;
    let top = y.floor() as isize;
    let fx = x - x.floor();
    let fy = y - y.floor();
    let pixel = |x: isize, y: isize| {
        if x < 0 || y < 0 || x >= width as isize || y >= height as isize {
            0.0
        } else {
            mask[y as usize * width + x as usize] as f32
        }
    };
    (pixel(left, top) * (1.0 - fx) + pixel(left + 1, top) * fx) * (1.0 - fy)
        + (pixel(left, top + 1) * (1.0 - fx) + pixel(left + 1, top + 1) * fx) * fy
}

fn spread_mask(mask: &[u8], width: usize, height: usize, radius: usize, dilate: bool) -> Vec<u8> {
    let mut horizontal = vec![0; mask.len()];
    let mut output = vec![0; mask.len()];
    for y in 0..height {
        extrema_line(
            width,
            radius,
            dilate,
            |x| mask[y * width + x],
            |x, value| horizontal[y * width + x] = value,
        );
    }
    for x in 0..width {
        extrema_line(
            height,
            radius,
            dilate,
            |y| horizontal[y * width + x],
            |y, value| output[y * width + x] = value,
        );
    }
    output
}

// Sliding extrema keep morphology linear in the surface size, even for wide spreads.
fn extrema_line(
    length: usize,
    radius: usize,
    dilate: bool,
    read: impl Fn(usize) -> u8,
    mut write: impl FnMut(usize, u8),
) {
    let radius = radius.min(length);
    let mut deque: VecDeque<(usize, u8)> = VecDeque::new();
    for index in 0..length + radius * 2 {
        let value = index
            .checked_sub(radius)
            .filter(|i| *i < length)
            .map(&read)
            .unwrap_or(0);
        while deque.back().is_some_and(|(_, previous)| {
            if dilate {
                *previous <= value
            } else {
                *previous >= value
            }
        }) {
            deque.pop_back();
        }
        deque.push_back((index, value));
        while deque
            .front()
            .is_some_and(|(previous, _)| *previous + radius * 2 < index)
        {
            deque.pop_front();
        }
        if index >= radius * 2 {
            write(
                index - radius * 2,
                deque.front().expect("nonempty window").1,
            );
        }
    }
}

#[cfg(test)]
#[path = "shadow_test.rs"]
mod tests;