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