picture.rs (5123B)
1 use crate::gif::{handle_repaint, retrieve_latest_texture}; 2 use crate::images::{fetch_no_pfp_promise, get_render_state, ImageType}; 3 use egui::{vec2, InnerResponse, Sense, Stroke, TextureHandle}; 4 5 use notedeck::note::MediaAction; 6 use notedeck::{show_one_error_message, supported_mime_hosted_at_url, Images}; 7 8 pub struct ProfilePic<'cache, 'url> { 9 cache: &'cache mut Images, 10 url: &'url str, 11 size: f32, 12 sense: Sense, 13 border: Option<Stroke>, 14 pub action: Option<MediaAction>, 15 } 16 17 impl egui::Widget for &mut ProfilePic<'_, '_> { 18 fn ui(self, ui: &mut egui::Ui) -> egui::Response { 19 let inner = render_pfp(ui, self.cache, self.url, self.size, self.border, self.sense); 20 21 self.action = inner.inner; 22 23 inner.response 24 } 25 } 26 27 impl<'cache, 'url> ProfilePic<'cache, 'url> { 28 pub fn new(cache: &'cache mut Images, url: &'url str) -> Self { 29 let size = Self::default_size() as f32; 30 let sense = Sense::hover(); 31 32 ProfilePic { 33 cache, 34 sense, 35 url, 36 size, 37 border: None, 38 action: None, 39 } 40 } 41 42 pub fn sense(mut self, sense: Sense) -> Self { 43 self.sense = sense; 44 self 45 } 46 47 pub fn border_stroke(ui: &egui::Ui) -> Stroke { 48 Stroke::new(4.0, ui.visuals().panel_fill) 49 } 50 51 pub fn from_profile( 52 cache: &'cache mut Images, 53 profile: &nostrdb::ProfileRecord<'url>, 54 ) -> Option<Self> { 55 profile 56 .record() 57 .profile() 58 .and_then(|p| p.picture()) 59 .map(|url| ProfilePic::new(cache, url)) 60 } 61 62 pub fn from_profile_or_default( 63 cache: &'cache mut Images, 64 profile: Option<&nostrdb::ProfileRecord<'url>>, 65 ) -> Self { 66 let url = profile 67 .map(|p| p.record()) 68 .and_then(|p| p.profile()) 69 .and_then(|p| p.picture()) 70 .unwrap_or(notedeck::profile::no_pfp_url()); 71 72 ProfilePic::new(cache, url) 73 } 74 75 #[inline] 76 pub fn default_size() -> i8 { 77 38 78 } 79 80 #[inline] 81 pub fn medium_size() -> i8 { 82 32 83 } 84 85 #[inline] 86 pub fn small_size() -> i8 { 87 24 88 } 89 90 #[inline] 91 pub fn size(mut self, size: f32) -> Self { 92 self.size = size; 93 self 94 } 95 96 #[inline] 97 pub fn border(mut self, stroke: Stroke) -> Self { 98 self.border = Some(stroke); 99 self 100 } 101 } 102 103 #[profiling::function] 104 fn render_pfp( 105 ui: &mut egui::Ui, 106 img_cache: &mut Images, 107 url: &str, 108 ui_size: f32, 109 border: Option<Stroke>, 110 sense: Sense, 111 ) -> InnerResponse<Option<MediaAction>> { 112 // We will want to downsample these so it's not blurry on hi res displays 113 let img_size = 128u32; 114 115 let cache_type = supported_mime_hosted_at_url(&mut img_cache.urls, url) 116 .unwrap_or(notedeck::MediaCacheType::Image); 117 118 let cur_state = get_render_state( 119 ui.ctx(), 120 img_cache, 121 cache_type, 122 url, 123 ImageType::Profile(img_size), 124 ); 125 126 match cur_state.texture_state { 127 notedeck::TextureState::Pending => { 128 egui::InnerResponse::new(None, paint_circle(ui, ui_size, border, sense)) 129 } 130 notedeck::TextureState::Error(e) => { 131 let r = paint_circle(ui, ui_size, border, sense); 132 show_one_error_message(ui, &format!("Failed to fetch profile at url {url}: {e}")); 133 egui::InnerResponse::new( 134 Some(MediaAction::FetchImage { 135 url: url.to_owned(), 136 cache_type, 137 no_pfp_promise: fetch_no_pfp_promise(ui.ctx(), img_cache.get_cache(cache_type)), 138 }), 139 r, 140 ) 141 } 142 notedeck::TextureState::Loaded(textured_image) => { 143 let texture_handle = handle_repaint( 144 ui, 145 retrieve_latest_texture(url, cur_state.gifs, textured_image), 146 ); 147 148 egui::InnerResponse::new(None, pfp_image(ui, texture_handle, ui_size, border, sense)) 149 } 150 } 151 } 152 153 #[profiling::function] 154 fn pfp_image( 155 ui: &mut egui::Ui, 156 img: &TextureHandle, 157 size: f32, 158 border: Option<Stroke>, 159 sense: Sense, 160 ) -> egui::Response { 161 let (rect, response) = ui.allocate_at_least(vec2(size, size), sense); 162 if let Some(stroke) = border { 163 draw_bg_border(ui, rect.center(), size, stroke); 164 } 165 ui.put(rect, egui::Image::new(img).max_width(size)); 166 167 response 168 } 169 170 fn paint_circle( 171 ui: &mut egui::Ui, 172 size: f32, 173 border: Option<Stroke>, 174 sense: Sense, 175 ) -> egui::Response { 176 let (rect, response) = ui.allocate_at_least(vec2(size, size), sense); 177 178 if let Some(stroke) = border { 179 draw_bg_border(ui, rect.center(), size, stroke); 180 } 181 182 ui.painter() 183 .circle_filled(rect.center(), size / 2.0, ui.visuals().weak_text_color()); 184 185 response 186 } 187 188 fn draw_bg_border(ui: &mut egui::Ui, center: egui::Pos2, size: f32, stroke: Stroke) { 189 let border_size = size + (stroke.width * 2.0); 190 ui.painter() 191 .circle_filled(center, border_size / 2.0, stroke.color); 192 }