notedeck

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

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 }