halley-wl 0.3.1

Wayland backend and rendering implementation for the Halley Wayland compositor.
use image::RgbaImage;
use resvg::{tiny_skia, usvg};
use smithay::backend::allocator::Fourcc;
use smithay::backend::renderer::gles::GlesFrame;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::backend::renderer::{Color32F, ImportMem};
use smithay::utils::{Buffer, Physical, Rectangle, Transform};

use crate::compositor::root::Halley;
use crate::render::icon_tint::tint_alpha_mask_image;
use crate::render::state::{NodeAppIconTexture, PinIconCache};

const PIN_ICON_RASTER_PX: u32 = 64;
const PIN_GLYPH_DIAMETER_FRACTION: f32 = 0.62;
const PIN_SVG: &[u8] = include_bytes!("assets/pin.svg");

#[derive(Clone, Copy, Debug)]
pub(crate) struct PinBadgeLayout {
    pub(crate) cx: i32,
    pub(crate) cy: i32,
    pub(crate) radius: i32,
    pub(crate) alpha: f32,
}

pub(crate) fn ensure_pin_icon_resources(
    renderer: &mut GlesRenderer,
    st: &mut Halley,
) -> Result<(), Box<dyn std::error::Error>> {
    let color = pin_rgba(&st.runtime.tuning);
    if st.ui.render_state.cache.pin_icon_cache.color == color
        && st.ui.render_state.cache.pin_icon_cache.icon.is_some()
    {
        return Ok(());
    }

    st.ui.render_state.cache.pin_icon_cache = PinIconCache {
        color,
        icon: load_pin_icon_texture(renderer, color)?,
    };
    Ok(())
}

pub(crate) fn pin_icon_texture(st: &Halley) -> Option<&NodeAppIconTexture> {
    st.ui.render_state.cache.pin_icon_cache.icon.as_ref()
}

pub(crate) fn scaled_pin_badge_radius(st: &Halley, radius: i32) -> i32 {
    ((radius as f32) * st.runtime.tuning.pins.size.clamp(0.5, 3.0))
        .round()
        .max(1.0) as i32
}

pub(crate) fn pin_badge_fill_color(
    st: &Halley,
    alpha: f32,
) -> smithay::backend::renderer::Color32F {
    let fill = pin_badge_fill_rgb(&st.runtime.tuning);
    smithay::backend::renderer::Color32F::new(fill.0, fill.1, fill.2, alpha)
}

pub(crate) fn draw_pin_badges(
    frame: &mut GlesFrame<'_, '_>,
    st: &Halley,
    layouts: &[PinBadgeLayout],
    damage: Rectangle<i32, Physical>,
) -> Result<(), Box<dyn std::error::Error>> {
    for layout in layouts {
        draw_pin_badge(frame, st, *layout, damage)?;
    }
    Ok(())
}

pub(crate) fn draw_pin_badge(
    frame: &mut GlesFrame<'_, '_>,
    st: &Halley,
    layout: PinBadgeLayout,
    damage: Rectangle<i32, Physical>,
) -> Result<(), Box<dyn std::error::Error>> {
    let alpha = layout.alpha.clamp(0.0, 1.0);
    if alpha <= 0.01 {
        return Ok(());
    }

    let fill = pin_badge_fill_color(st, 0.90 * alpha);
    super::node::draw_shader_circle(
        frame,
        st,
        layout.cx,
        layout.cy,
        layout.radius.max(1),
        super::node::NodeRoundShape::Circle,
        alpha,
        Color32F::new(fill.r(), fill.g(), fill.b(), 0.0),
        fill,
        false,
        false,
        damage,
    )?;

    let Some(icon) = pin_icon_texture(st) else {
        return Ok(());
    };
    let side = ((layout.radius * 2) as f32 * PIN_GLYPH_DIAMETER_FRACTION)
        .round()
        .max(1.0) as i32;
    let dest = Rectangle::<i32, Physical>::new(
        (layout.cx - side / 2, layout.cy - side / 2).into(),
        (side, side).into(),
    );
    let src = Rectangle::<f64, Buffer>::new(
        (0.0, 0.0).into(),
        (icon.width as f64, icon.height as f64).into(),
    );
    frame.render_texture_from_to(
        &icon.texture,
        src,
        dest,
        &[damage],
        &[],
        Transform::Normal,
        alpha,
        None,
        &[],
    )?;
    Ok(())
}

fn pin_rgba(tuning: &halley_config::RuntimeTuning) -> [u8; 4] {
    let color = pin_glyph_rgb(tuning);
    [
        (color.0.clamp(0.0, 1.0) * 255.0).round() as u8,
        (color.1.clamp(0.0, 1.0) * 255.0).round() as u8,
        (color.2.clamp(0.0, 1.0) * 255.0).round() as u8,
        255,
    ]
}

fn pin_glyph_rgb(tuning: &halley_config::RuntimeTuning) -> (f32, f32, f32) {
    let color = match tuning.pins.color {
        halley_config::OverlayColorMode::Auto => {
            let (_, text) = if matches!(
                tuning.pins.background_color,
                halley_config::OverlayColorMode::Auto
            ) {
                crate::overlay::overlay_fill_and_text_colors(tuning)
            } else {
                let fill = pin_badge_fill_rgb(tuning);
                let fill = Color32F::new(fill.0, fill.1, fill.2, 1.0);
                (fill, crate::overlay::overlay_text_color_for_fill(fill, 1.0))
            };
            return (text.r(), text.g(), text.b());
        }
        halley_config::OverlayColorMode::Light => (0.08, 0.10, 0.12),
        halley_config::OverlayColorMode::Dark => (0.94, 0.96, 0.98),
        halley_config::OverlayColorMode::Fixed { r, g, b } => (r, g, b),
    };
    (
        color.0.clamp(0.0, 1.0),
        color.1.clamp(0.0, 1.0),
        color.2.clamp(0.0, 1.0),
    )
}

fn pin_badge_fill_rgb(tuning: &halley_config::RuntimeTuning) -> (f32, f32, f32) {
    let color = match tuning.pins.background_color {
        halley_config::OverlayColorMode::Auto => {
            let (fill, _) = crate::overlay::overlay_fill_and_text_colors(tuning);
            return (fill.r(), fill.g(), fill.b());
        }
        halley_config::OverlayColorMode::Light => (0.92, 0.95, 0.98),
        halley_config::OverlayColorMode::Dark => (0.15, 0.18, 0.22),
        halley_config::OverlayColorMode::Fixed { r, g, b } => (r, g, b),
    };
    (
        color.0.clamp(0.0, 1.0),
        color.1.clamp(0.0, 1.0),
        color.2.clamp(0.0, 1.0),
    )
}

fn load_pin_icon_texture(
    renderer: &mut GlesRenderer,
    rgba: [u8; 4],
) -> Result<Option<NodeAppIconTexture>, Box<dyn std::error::Error>> {
    let Some(raster) = load_pin_icon_raster(rgba) else {
        return Ok(None);
    };
    let texture = renderer.import_memory(
        &raster.into_vec(),
        Fourcc::Abgr8888,
        (PIN_ICON_RASTER_PX as i32, PIN_ICON_RASTER_PX as i32).into(),
        false,
    )?;
    Ok(Some(NodeAppIconTexture {
        texture,
        width: PIN_ICON_RASTER_PX as i32,
        height: PIN_ICON_RASTER_PX as i32,
    }))
}

fn load_pin_icon_raster(rgba: [u8; 4]) -> Option<RgbaImage> {
    let mut options = usvg::Options::default();
    options.fontdb_mut().load_system_fonts();
    let tree = usvg::Tree::from_data(PIN_SVG, &options).ok()?;
    let svg_size = tree.size().to_int_size();
    if svg_size.width() == 0 || svg_size.height() == 0 {
        return None;
    }

    let mut pixmap = tiny_skia::Pixmap::new(PIN_ICON_RASTER_PX, PIN_ICON_RASTER_PX)?;
    let scale_x = PIN_ICON_RASTER_PX as f32 / svg_size.width() as f32;
    let scale_y = PIN_ICON_RASTER_PX as f32 / svg_size.height() as f32;
    let scale = scale_x.min(scale_y);
    let dx = (PIN_ICON_RASTER_PX as f32 - svg_size.width() as f32 * scale) * 0.5;
    let dy = (PIN_ICON_RASTER_PX as f32 - svg_size.height() as f32 * scale) * 0.5;
    let transform = tiny_skia::Transform::from_scale(scale, scale).post_translate(dx, dy);
    resvg::render(&tree, transform, &mut pixmap.as_mut());

    let mut image = RgbaImage::from_vec(
        PIN_ICON_RASTER_PX,
        PIN_ICON_RASTER_PX,
        pixmap.data().to_vec(),
    )?;
    tint_alpha_mask_image(&mut image, rgba);
    Some(image)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn fixed_pin_color_controls_glyph_rgba() {
        let mut tuning = halley_config::RuntimeTuning::default();
        tuning.pins.color = halley_config::OverlayColorMode::Fixed {
            r: 1.0,
            g: 0.5,
            b: 0.0,
        };

        assert_eq!(pin_rgba(&tuning), [255, 128, 0, 255]);
    }

    #[test]
    fn fixed_pin_background_color_controls_badge_fill() {
        let mut tuning = halley_config::RuntimeTuning::default();
        tuning.pins.background_color = halley_config::OverlayColorMode::Fixed {
            r: 0.25,
            g: 0.5,
            b: 0.75,
        };

        assert_eq!(pin_badge_fill_rgb(&tuning), (0.25, 0.5, 0.75));
    }
}