//! The start wallpaper: the image Settings → Appearance can put behind the
//! new-thread screen. It's decoded once on a background thread, scaled down
//! to what a screen can show, and kept until the file changes, so drawing it
//! costs nothing after the first frame.
use std::{
collections::HashMap,
path::{Path, PathBuf},
sync::Arc,
time::SystemTime,
};
use gpui_kit::*;
/// Wider or taller images are scaled down to this before they're kept.
const MAX_SIDE: u32 = 2560;
/// A wallpaper by path, as it was when last read.
enum Entry {
Loading(Option<SystemTime>),
Ready(Option<SystemTime>, Arc<RenderImage>),
Failed(Option<SystemTime>),
}
impl Entry {
fn modified(&self) -> Option<SystemTime> {
match self {
Entry::Loading(modified) | Entry::Ready(modified, _) | Entry::Failed(modified) => *modified,
}
}
}
#[derive(Default)]
struct Wallpapers {
entries: HashMap<PathBuf, Entry>,
}
impl Global for Wallpapers {}
/// The wallpaper at `path`, once it's decoded. The first call starts
/// decoding it and `view` repaints once it's ready; a file that changed
/// since is read again.
pub fn image(path: &Path, view: EntityId, cx: &mut App) -> Option<Arc<RenderImage>> {
let modified = std::fs::metadata(path).and_then(|meta| meta.modified()).ok();
let cache = cx.default_global::<Wallpapers>();
match cache.entries.get(path) {
Some(entry) if entry.modified() == modified => {
return match entry {
Entry::Ready(_, image) => Some(image.clone()),
_ => None,
};
}
_ => {}
}
cache.entries.insert(path.to_path_buf(), Entry::Loading(modified));
let file = path.to_path_buf();
let decode = cx.background_executor().spawn(async move { decode(&file) });
let path = path.to_path_buf();
cx.spawn(async move |cx| {
let result = decode.await;
cx.update(|cx| {
let entry = match result {
Ok(image) => Entry::Ready(modified, Arc::new(image)),
Err(err) => {
eprintln!("vectrum-code: couldn't load wallpaper {}: {err}", path.display());
Entry::Failed(modified)
}
};
cx.default_global::<Wallpapers>().entries.insert(path, entry);
cx.notify(view);
cx.refresh_windows();
});
})
.detach();
None
}
/// Reads an image file into the BGRA frame GPUI draws, no larger than
/// [`MAX_SIDE`] on either side.
fn decode(path: &Path) -> anyhow::Result<RenderImage> {
let decoded = ::image::ImageReader::open(path)?.with_guessed_format()?.decode()?;
let decoded = if decoded.width() > MAX_SIDE || decoded.height() > MAX_SIDE {
decoded.resize(MAX_SIDE, MAX_SIDE, ::image::imageops::FilterType::Triangle)
} else {
decoded
};
let mut pixels = decoded.into_rgba8();
for pixel in pixels.chunks_exact_mut(4) {
pixel.swap(0, 2);
}
Ok(RenderImage::new(vec![::image::Frame::new(pixels)]))
}
#[cfg(test)]
mod tests {
use super::{MAX_SIDE, decode};
#[::core::prelude::v1::test]
fn decodes_and_scales_down() {
let dir = std::env::temp_dir().join(format!("vectrum-wallpaper-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let file = dir.join("wide.png");
::image::RgbaImage::from_pixel(4000, 1000, ::image::Rgba([255, 0, 0, 255])).save(&file).unwrap();
let image = decode(&file).unwrap();
let size = image.size(0);
assert_eq!((size.width.0, size.height.0), (MAX_SIDE as i32, MAX_SIDE as i32 / 4));
// Red comes out as BGRA.
assert_eq!(&image.as_bytes(0).unwrap()[..4], &[0, 0, 255, 255]);
assert!(decode(&dir.join("missing.png")).is_err());
let _ = std::fs::remove_file(&file);
}
}