notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

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 }