//! 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), Ready(Option, Arc), Failed(Option), } impl Entry { fn modified(&self) -> Option { match self { Entry::Loading(modified) | Entry::Ready(modified, _) | Entry::Failed(modified) => *modified, } } } #[derive(Default)] struct Wallpapers { entries: HashMap, } 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> { let modified = std::fs::metadata(path).and_then(|meta| meta.modified()).ok(); let cache = cx.default_global::(); 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::().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 { 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); } }