notedeck

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

picture.rs (5123B)


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