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