diff --git a/src/images.rs b/src/images.rs index 5a95aae..b507f4c 100644 --- a/src/images.rs +++ b/src/images.rs @@ -102,54 +102,72 @@ pub fn round_image(image: &mut ColorImage) { } } -fn process_pfp_bitmap(size: u32, image: &mut image::DynamicImage) -> ColorImage { +fn process_pfp_bitmap(imgtyp: ImageType, image: &mut image::DynamicImage) -> ColorImage { #[cfg(feature = "profiling")] puffin::profile_function!(); - // Crop square - let smaller = image.width().min(image.height()); + match imgtyp { + ImageType::Content(w, h) => { + let image = image.resize(w, h, FilterType::CatmullRom); // DynamicImage + let image_buffer = image.into_rgba8(); // RgbaImage (ImageBuffer) + let color_image = ColorImage::from_rgba_unmultiplied( + [ + image_buffer.width() as usize, + image_buffer.height() as usize, + ], + image_buffer.as_flat_samples().as_slice(), + ); + color_image + } + ImageType::Profile(size) => { + // Crop square + let smaller = image.width().min(image.height()); - if image.width() > smaller { - let excess = image.width() - smaller; - *image = image.crop_imm(excess / 2, 0, image.width() - excess, image.height()); - } else if image.height() > smaller { - let excess = image.height() - smaller; - *image = image.crop_imm(0, excess / 2, image.width(), image.height() - excess); + if image.width() > smaller { + let excess = image.width() - smaller; + *image = image.crop_imm(excess / 2, 0, image.width() - excess, image.height()); + } else if image.height() > smaller { + let excess = image.height() - smaller; + *image = image.crop_imm(0, excess / 2, image.width(), image.height() - excess); + } + let image = image.resize(size, size, FilterType::CatmullRom); // DynamicImage + let image_buffer = image.into_rgba8(); // RgbaImage (ImageBuffer) + let mut color_image = ColorImage::from_rgba_unmultiplied( + [ + image_buffer.width() as usize, + image_buffer.height() as usize, + ], + image_buffer.as_flat_samples().as_slice(), + ); + round_image(&mut color_image); + color_image + } } - let image = image.resize(size, size, FilterType::CatmullRom); // DynamicImage - let image_buffer = image.into_rgba8(); // RgbaImage (ImageBuffer) - let mut color_image = ColorImage::from_rgba_unmultiplied( - [ - image_buffer.width() as usize, - image_buffer.height() as usize, - ], - image_buffer.as_flat_samples().as_slice(), - ); - round_image(&mut color_image); - color_image } -fn parse_img_response(response: ehttp::Response, size: u32) -> Result { +fn parse_img_response(response: ehttp::Response, imgtyp: ImageType) -> Result { #[cfg(feature = "profiling")] puffin::profile_function!(); let content_type = response.content_type().unwrap_or_default(); + let size_hint = match imgtyp { + ImageType::Profile(size) => SizeHint::Size(size, size), + ImageType::Content(w, h) => SizeHint::Size(w, h), + }; if content_type.starts_with("image/svg") { #[cfg(feature = "profiling")] puffin::profile_scope!("load_svg"); - let mut color_image = egui_extras::image::load_svg_bytes_with_size( - &response.bytes, - Some(SizeHint::Size(size, size)), - )?; + let mut color_image = + egui_extras::image::load_svg_bytes_with_size(&response.bytes, Some(size_hint))?; round_image(&mut color_image); Ok(color_image) } else if content_type.starts_with("image/") { #[cfg(feature = "profiling")] puffin::profile_scope!("load_from_memory"); let mut dyn_image = image::load_from_memory(&response.bytes)?; - Ok(process_pfp_bitmap(size, &mut dyn_image)) + Ok(process_pfp_bitmap(imgtyp, &mut dyn_image)) } else { Err(format!("Expected image, found content-type {:?}", content_type).into()) } @@ -181,11 +199,20 @@ fn fetch_img_from_disk( }) } +/// Controls type-specific handling +#[derive(Debug, Clone, Copy)] +pub enum ImageType { + /// Profile Image (size) + Profile(u32), + /// Content Image (width, height) + Content(u32, u32), +} + pub fn fetch_img( img_cache: &ImageCache, ctx: &egui::Context, url: &str, - size: u32, + imgtyp: ImageType, ) -> Promise> { let key = ImageCache::key(url); let path = img_cache.cache_dir.join(key); @@ -193,7 +220,7 @@ pub fn fetch_img( if path.exists() { fetch_img_from_disk(ctx, url, &path) } else { - fetch_img_from_net(&img_cache.cache_dir, ctx, url, size) + fetch_img_from_net(&img_cache.cache_dir, ctx, url, imgtyp) } // TODO: fetch image from local cache @@ -203,7 +230,7 @@ fn fetch_img_from_net( cache_path: &path::Path, ctx: &egui::Context, url: &str, - size: u32, + imgtyp: ImageType, ) -> Promise> { let (sender, promise) = Promise::new(); let request = ehttp::Request::get(url); @@ -213,7 +240,7 @@ fn fetch_img_from_net( ehttp::fetch(request, move |response| { let handle = response .map_err(Error::Generic) - .and_then(|resp| parse_img_response(resp, size)) + .and_then(|resp| parse_img_response(resp, imgtyp)) .map(|img| { let texture_handle = ctx.load_texture(&cloned_url, img.clone(), Default::default()); diff --git a/src/ui/note/contents.rs b/src/ui/note/contents.rs index 88c4593..8be574b 100644 --- a/src/ui/note/contents.rs +++ b/src/ui/note/contents.rs @@ -1,4 +1,7 @@ +use crate::images::ImageType; +use crate::imgcache::ImageCache; use crate::ui::note::NoteOptions; +use crate::ui::ProfilePic; use crate::{colors, ui, Damus}; use egui::{Color32, Hyperlink, Image, RichText}; use nostrdb::{BlockType, Mention, Note, NoteKey, Transaction}; @@ -189,18 +192,24 @@ fn render_note_contents( if !images.is_empty() && !damus.textmode { ui.add_space(2.0); let carousel_id = egui::Id::new(("carousel", note.key().expect("expected tx note"))); - image_carousel(ui, images, carousel_id); + image_carousel(ui, &mut damus.img_cache, images, carousel_id); ui.add_space(2.0); } resp } -fn image_carousel(ui: &mut egui::Ui, images: Vec, carousel_id: egui::Id) { +fn image_carousel( + ui: &mut egui::Ui, + img_cache: &mut ImageCache, + images: Vec, + carousel_id: egui::Id, +) { // let's make sure everything is within our area let height = 360.0; let width = ui.available_size().x; + let spinsz = if height > width { width } else { height }; ui.add_sized([width, height], |ui: &mut egui::Ui| { egui::ScrollArea::horizontal() @@ -208,18 +217,53 @@ fn image_carousel(ui: &mut egui::Ui, images: Vec, carousel_id: egui::Id) .show(ui, |ui| { ui.horizontal(|ui| { for image in images { - let img_resp = ui.add( - Image::new(image.clone()) - .max_height(height) - .rounding(5.0) - .fit_to_original_size(1.0), - ); - img_resp.context_menu(|ui| { - if ui.button("Copy Link").clicked() { - ui.ctx().copy_text(image); - ui.close_menu(); + // If the cache is empty, initiate the fetch + let m_cached_promise = img_cache.map().get(&image); + if m_cached_promise.is_none() { + let res = crate::images::fetch_img( + img_cache, + ui.ctx(), + &image, + ImageType::Content(width.round() as u32, height.round() as u32), + ); + img_cache.map_mut().insert(image.to_owned(), res); + } + + // What is the state of the fetch? + match img_cache.map()[&image].ready() { + // Still waiting + None => { + ui.add(egui::Spinner::new().size(spinsz)); } - }); + // Failed to fetch image! + Some(Err(_err)) => { + // FIXME - use content-specific error instead + let no_pfp = crate::images::fetch_img( + img_cache, + ui.ctx(), + ProfilePic::no_pfp_url(), + ImageType::Profile(128), + ); + img_cache.map_mut().insert(image.to_owned(), no_pfp); + // spin until next pass + ui.add(egui::Spinner::new().size(spinsz)); + } + // Use the previously resolved image + Some(Ok(img)) => { + let img_resp = ui.add( + Image::new(img) + .max_height(height) + .rounding(5.0) + .fit_to_original_size(1.0), + ); + img_resp.context_menu(|ui| { + if ui.button("Copy Link").clicked() { + ui.ctx().copy_text(image); + ui.close_menu(); + } + }); + } + } } }) .response diff --git a/src/ui/profile/picture.rs b/src/ui/profile/picture.rs index 69fc72d..86ef55a 100644 --- a/src/ui/profile/picture.rs +++ b/src/ui/profile/picture.rs @@ -1,3 +1,4 @@ +use crate::images::ImageType; use crate::imgcache::ImageCache; use crate::ui::{Preview, PreviewConfig, View}; use egui::{vec2, Sense, TextureHandle}; @@ -72,7 +73,7 @@ fn render_pfp( let m_cached_promise = img_cache.map().get(url); if m_cached_promise.is_none() { - let res = crate::images::fetch_img(img_cache, ui.ctx(), url, img_size); + let res = crate::images::fetch_img(img_cache, ui.ctx(), url, ImageType::Profile(img_size)); img_cache.map_mut().insert(url.to_owned(), res); } @@ -87,7 +88,7 @@ fn render_pfp( img_cache, ui.ctx(), ProfilePic::no_pfp_url(), - img_size, + ImageType::Profile(img_size), ); img_cache.map_mut().insert(url.to_owned(), no_pfp); }