yofi 0.2.0

minimalistic menu for wayland
use std::io::BufReader;
use std::path::{Path, PathBuf};

use anyhow::{anyhow, ensure, Context, Result};
use once_cell::unsync::Lazy;

pub struct Loaded {
    width: u32,
    height: u32,
    data: Vec<u32>,
}

pub enum IconInner {
    Pending(PathBuf),
    Failed,
    Loaded(Loaded),
}

pub struct Icon {
    inner: Lazy<Option<IconInner>, Box<dyn FnOnce() -> Option<IconInner>>>,
}

impl Icon {
    pub fn new(path: impl Into<PathBuf>) -> Self {
        let path = path.into();
        Self {
            inner: Lazy::new(Box::new(move || {
                let mut inner = IconInner::new(path);
                inner.load().map(|()| inner)
            })),
        }
    }

    pub fn as_image(&self) -> Option<raqote::Image> {
        Lazy::force(&self.inner)
            .as_ref()?
            .loaded()
            .map(|l| l.as_image())
    }
}

impl Default for IconInner {
    fn default() -> Self {
        Self::Failed
    }
}

impl IconInner {
    pub fn new(path: impl Into<PathBuf>) -> Self {
        Self::Pending(path.into())
    }

    fn loaded(&self) -> Option<&Loaded> {
        if let Self::Loaded(l) = self {
            Some(l)
        } else {
            unreachable!()
        }
    }

    fn load(&mut self) -> Option<()> {
        let loaded = match self {
            Self::Pending(path) => Loaded::load(path),
            Self::Failed => return None,
            Self::Loaded(_) => return Some(()),
        };

        if let Some(loaded) = loaded {
            *self = Self::Loaded(loaded);
            Some(())
        } else {
            *self = Self::Failed;
            None
        }
    }
}

impl Loaded {
    pub fn load(path: impl AsRef<Path>) -> Option<Self> {
        let path = path.as_ref();
        let failed_to_load = |e| log::info!("failed to load icon at path `{:?}`: {}", path, e);
        match path.extension()?.to_str()? {
            "png" => Self::from_png_path(path).map_err(failed_to_load).ok(),
            "svg" => Self::from_svg_path(path).map_err(failed_to_load).ok(),
            ext => {
                log::info!("unsupported icon extension: {:?}", ext);
                None
            }
        }
    }

    fn from_png_path(path: impl AsRef<Path>) -> Result<Self> {
        let decoder = png::Decoder::new(BufReader::new(std::fs::File::open(path)?));
        let mut reader = decoder
            .read_info()
            .map_err(|e| anyhow!("failed to read png info: {}", e))?;
        let mut buf = vec![0; reader.output_buffer_size()];
        let info = reader
            .next_frame(&mut buf)
            .map_err(|e| anyhow!("failed to read png frame: {}", e))?;

        let buf = &buf[..info.buffer_size()];
        let info = reader.info();

        let data = match info.color_type {
            png::ColorType::Rgb => {
                ensure!(buf.len() % 3 == 0, "corrupted icon file");

                buf.chunks(3)
                    .map(|chunk| {
                        let a = 0xffu32 << 24;
                        let r = u32::from(chunk[0]) << 16;
                        let g = u32::from(chunk[1]) << 8;
                        let b = u32::from(chunk[2]);

                        a | r | g | b
                    })
                    .collect()
            }
            png::ColorType::Rgba => rgba_to_argb(buf)?,
            png::ColorType::GrayscaleAlpha => {
                ensure!(buf.len() % 2 == 0, "corrupted icon file");

                buf.chunks(2)
                    .map(|chunk| {
                        let x = u32::from(chunk[0]);
                        let a = u32::from(chunk[1]) << 24;

                        a | (x << 16) | (x << 8) | x
                    })
                    .collect()
            }
            png::ColorType::Grayscale => buf
                .iter()
                .copied()
                .map(u32::from)
                .map(|chunk| {
                    let a = 0xffu32 << 24;
                    let r = chunk << 16;
                    let g = chunk << 8;
                    let b = chunk;

                    a | r | g | b
                })
                .collect(),
            png::ColorType::Indexed => {
                let palette = info.palette.as_ref().ok_or_else(|| {
                    anyhow!("invalid image: palette is missing for indexed color type")
                })?;

                buf.iter()
                    .copied()
                    .map(|idx| {
                        let start = 3 * usize::from(idx);
                        let end = start + 3;
                        let chunk = palette
                            .get(start..end)
                            .context("corrupted icon file (palette overflow)")?;
                        let a = 0xffu32 << 24;
                        let r = u32::from(chunk[0]) << 16;
                        let g = u32::from(chunk[1]) << 8;
                        let b = u32::from(chunk[2]);

                        Ok(a | r | g | b)
                    })
                    .collect::<Result<_>>()?
            }
        };

        Ok(Self {
            width: info.width,
            height: info.height,
            data,
        })
    }

    fn from_svg_path(path: impl AsRef<Path>) -> Result<Self> {
        let opt = usvg::Options::default();
        let data = std::fs::read(path.as_ref())
            .with_context(|| format!("failed to open svg file: {:?}", path.as_ref()))?;
        let tree = usvg::Tree::from_data(&data, &opt.to_ref())
            .map_err(|e| anyhow!("svg open error: {}", e))?;

        let width = tree.svg_node().size.width().ceil() as u32;
        let height = tree.svg_node().size.height().ceil() as u32;
        let mut buf = tiny_skia::Pixmap::new(width, height).context("invalid pixmap size")?;
        resvg::render(
            &tree,
            usvg::FitTo::Original,
            tiny_skia::Transform::identity(),
            buf.as_mut(),
        )
        .ok_or_else(|| anyhow!("cannot render svg"))?;

        Ok(Self {
            width,
            height,
            data: rgba_to_argb(buf.data())?,
        })
    }

    fn as_image(&self) -> raqote::Image {
        raqote::Image {
            width: self.width as i32,
            height: self.height as i32,
            data: self.data.as_slice(),
        }
    }
}

fn rgba_to_argb(buf: &[u8]) -> Result<Vec<u32>> {
    ensure!(buf.len() % 4 == 0, "corrupted icon file");

    let data = buf
        .chunks(4)
        .map(|chunk| {
            let src = raqote::SolidSource::from_unpremultiplied_argb(
                chunk[3], chunk[0], chunk[1], chunk[2],
            );

            let a = u32::from(src.a) << 24;
            let r = u32::from(src.r) << 16;
            let g = u32::from(src.g) << 8;
            let b = u32::from(src.b);

            a | r | g | b
        })
        .collect();

    Ok(data)
}