picture.rs (7594B)
1 use crate::gif::{handle_repaint, retrieve_latest_texture}; 2 use crate::images::ImageType; 3 use crate::ui::images::render_images; 4 use crate::ui::{Preview, PreviewConfig}; 5 use egui::{vec2, Sense, Stroke, TextureHandle}; 6 use nostrdb::{Ndb, Transaction}; 7 use tracing::info; 8 9 use notedeck::{supported_mime_hosted_at_url, AppContext, Images}; 10 11 pub struct ProfilePic<'cache, 'url> { 12 cache: &'cache mut Images, 13 url: &'url str, 14 size: f32, 15 border: Option<Stroke>, 16 } 17 18 impl egui::Widget for ProfilePic<'_, '_> { 19 fn ui(self, ui: &mut egui::Ui) -> egui::Response { 20 render_pfp(ui, self.cache, self.url, self.size, self.border) 21 } 22 } 23 24 impl<'cache, 'url> ProfilePic<'cache, 'url> { 25 pub fn new(cache: &'cache mut Images, url: &'url str) -> Self { 26 let size = Self::default_size(); 27 ProfilePic { 28 cache, 29 url, 30 size, 31 border: None, 32 } 33 } 34 35 pub fn border_stroke(ui: &egui::Ui) -> Stroke { 36 Stroke::new(4.0, ui.visuals().panel_fill) 37 } 38 39 pub fn from_profile( 40 cache: &'cache mut Images, 41 profile: &nostrdb::ProfileRecord<'url>, 42 ) -> Option<Self> { 43 profile 44 .record() 45 .profile() 46 .and_then(|p| p.picture()) 47 .map(|url| ProfilePic::new(cache, url)) 48 } 49 50 #[inline] 51 pub fn default_size() -> f32 { 52 38.0 53 } 54 55 #[inline] 56 pub fn medium_size() -> f32 { 57 32.0 58 } 59 60 #[inline] 61 pub fn small_size() -> f32 { 62 24.0 63 } 64 65 #[inline] 66 pub fn no_pfp_url() -> &'static str { 67 "https://damus.io/img/no-profile.svg" 68 } 69 70 #[inline] 71 pub fn size(mut self, size: f32) -> Self { 72 self.size = size; 73 self 74 } 75 76 #[inline] 77 pub fn border(mut self, stroke: Stroke) -> Self { 78 self.border = Some(stroke); 79 self 80 } 81 } 82 83 fn render_pfp( 84 ui: &mut egui::Ui, 85 img_cache: &mut Images, 86 url: &str, 87 ui_size: f32, 88 border: Option<Stroke>, 89 ) -> egui::Response { 90 #[cfg(feature = "profiling")] 91 puffin::profile_function!(); 92 93 // We will want to downsample these so it's not blurry on hi res displays 94 let img_size = 128u32; 95 96 let cache_type = supported_mime_hosted_at_url(&mut img_cache.urls, url) 97 .unwrap_or(notedeck::MediaCacheType::Image); 98 99 render_images( 100 ui, 101 img_cache, 102 url, 103 ImageType::Profile(img_size), 104 cache_type, 105 |ui| { 106 paint_circle(ui, ui_size, border); 107 }, 108 |ui, _| { 109 paint_circle(ui, ui_size, border); 110 }, 111 |ui, url, renderable_media, gifs| { 112 let texture_handle = 113 handle_repaint(ui, retrieve_latest_texture(url, gifs, renderable_media)); 114 pfp_image(ui, texture_handle, ui_size, border); 115 }, 116 ) 117 } 118 119 fn pfp_image( 120 ui: &mut egui::Ui, 121 img: &TextureHandle, 122 size: f32, 123 border: Option<Stroke>, 124 ) -> egui::Response { 125 #[cfg(feature = "profiling")] 126 puffin::profile_function!(); 127 128 let (rect, response) = ui.allocate_at_least(vec2(size, size), Sense::hover()); 129 if let Some(stroke) = border { 130 draw_bg_border(ui, rect.center(), size, stroke); 131 } 132 ui.put(rect, egui::Image::new(img).max_width(size)); 133 134 response 135 } 136 137 fn paint_circle(ui: &mut egui::Ui, size: f32, border: Option<Stroke>) -> egui::Response { 138 let (rect, response) = ui.allocate_at_least(vec2(size, size), Sense::hover()); 139 140 if let Some(stroke) = border { 141 draw_bg_border(ui, rect.center(), size, stroke); 142 } 143 144 ui.painter() 145 .circle_filled(rect.center(), size / 2.0, ui.visuals().weak_text_color()); 146 147 response 148 } 149 150 fn draw_bg_border(ui: &mut egui::Ui, center: egui::Pos2, size: f32, stroke: Stroke) { 151 let border_size = size + (stroke.width * 2.0); 152 ui.painter() 153 .circle_filled(center, border_size / 2.0, stroke.color); 154 } 155 156 mod preview { 157 use super::*; 158 use crate::ui; 159 use nostrdb::*; 160 use std::collections::HashSet; 161 162 pub struct ProfilePicPreview { 163 keys: Option<Vec<ProfileKey>>, 164 } 165 166 impl ProfilePicPreview { 167 fn new() -> Self { 168 ProfilePicPreview { keys: None } 169 } 170 171 fn show(&mut self, app: &mut AppContext<'_>, ui: &mut egui::Ui) { 172 egui::ScrollArea::both().show(ui, |ui| { 173 ui.horizontal_wrapped(|ui| { 174 let txn = Transaction::new(app.ndb).unwrap(); 175 176 let keys = if let Some(keys) = &self.keys { 177 keys 178 } else { 179 return; 180 }; 181 182 for key in keys { 183 let profile = app.ndb.get_profile_by_key(&txn, *key).unwrap(); 184 let url = profile 185 .record() 186 .profile() 187 .expect("should have profile") 188 .picture() 189 .expect("should have picture"); 190 191 let expand_size = 10.0; 192 let anim_speed = 0.05; 193 194 let (rect, size, _resp) = ui::anim::hover_expand( 195 ui, 196 egui::Id::new(profile.key().unwrap()), 197 ui::ProfilePic::default_size(), 198 expand_size, 199 anim_speed, 200 ); 201 202 ui.put( 203 rect, 204 ui::ProfilePic::new(app.img_cache, url) 205 .size(size) 206 .border(ui::ProfilePic::border_stroke(ui)), 207 ) 208 .on_hover_ui_at_pointer(|ui| { 209 ui.set_max_width(300.0); 210 ui.add(ui::ProfilePreview::new(&profile, app.img_cache)); 211 }); 212 } 213 }); 214 }); 215 } 216 217 fn setup(&mut self, ndb: &Ndb) { 218 let txn = Transaction::new(ndb).unwrap(); 219 let filters = vec![Filter::new().kinds(vec![0]).build()]; 220 let mut pks = HashSet::new(); 221 let mut keys = HashSet::new(); 222 223 for query_result in ndb.query(&txn, &filters, 20000).unwrap() { 224 pks.insert(query_result.note.pubkey()); 225 } 226 227 for pk in pks { 228 let profile = if let Ok(profile) = ndb.get_profile_by_pubkey(&txn, pk) { 229 profile 230 } else { 231 continue; 232 }; 233 234 if profile 235 .record() 236 .profile() 237 .and_then(|p| p.picture()) 238 .is_none() 239 { 240 continue; 241 } 242 243 keys.insert(profile.key().expect("should not be owned")); 244 } 245 246 let keys: Vec<ProfileKey> = keys.into_iter().collect(); 247 info!("Loaded {} profiles", keys.len()); 248 self.keys = Some(keys); 249 } 250 } 251 252 impl notedeck::App for ProfilePicPreview { 253 fn update(&mut self, ctx: &mut AppContext<'_>, ui: &mut egui::Ui) { 254 if self.keys.is_none() { 255 self.setup(ctx.ndb); 256 } 257 258 self.show(ctx, ui) 259 } 260 } 261 262 impl Preview for ProfilePic<'_, '_> { 263 type Prev = ProfilePicPreview; 264 265 fn preview(_cfg: PreviewConfig) -> Self::Prev { 266 ProfilePicPreview::new() 267 } 268 } 269 }