notedeck

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

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 }