notedeck

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

mod.rs (5480B)


      1 use nostrdb::ProfileRecord;
      2 
      3 pub mod context;
      4 pub mod name;
      5 pub mod picture;
      6 pub mod preview;
      7 
      8 pub use picture::ProfilePic;
      9 pub use preview::ProfilePreview;
     10 
     11 use egui::{Label, RichText, TextureHandle};
     12 use notedeck::media::images::ImageType;
     13 use notedeck::media::AnimationMode;
     14 use notedeck::{
     15     Images, IsFollowing, MediaJobSender, NostrName, NotedeckTextStyle, PointDimensions,
     16 };
     17 
     18 use crate::{app_images, colors, widgets::styled_button_toggleable};
     19 
     20 pub fn display_name_widget<'a>(
     21     name: &'a NostrName<'a>,
     22     add_placeholder_space: bool,
     23 ) -> impl egui::Widget + 'a {
     24     move |ui: &mut egui::Ui| -> egui::Response {
     25         let disp_resp = name.display_name.map(|disp_name| {
     26             ui.add(
     27                 Label::new(
     28                     RichText::new(disp_name).text_style(NotedeckTextStyle::Heading3.text_style()),
     29                 )
     30                 .selectable(false),
     31             )
     32         });
     33 
     34         let (username_resp, nip05_resp) = ui
     35             .horizontal_wrapped(|ui| {
     36                 let username_resp = name.username.map(|username| {
     37                     ui.add(
     38                         Label::new(
     39                             RichText::new(format!("@{username}"))
     40                                 .size(16.0)
     41                                 .color(crate::colors::MID_GRAY),
     42                         )
     43                         .selectable(false),
     44                     )
     45                 });
     46 
     47                 if name.username.is_some() && name.nip05.is_some() {
     48                     ui.end_row();
     49                 }
     50 
     51                 let nip05_resp = name.nip05.map(|nip05| {
     52                     ui.horizontal_wrapped(|ui| {
     53                         ui.spacing_mut().item_spacing.x = 2.0;
     54 
     55                         ui.add(app_images::verified_image());
     56 
     57                         ui.label(RichText::new(nip05).size(16.0).color(crate::colors::TEAL))
     58                             .on_hover_text(nip05)
     59                     })
     60                     .inner
     61                 });
     62 
     63                 (username_resp, nip05_resp)
     64             })
     65             .inner;
     66 
     67         let resp = match (disp_resp, username_resp, nip05_resp) {
     68             (Some(disp), Some(username), Some(nip05)) => disp.union(username).union(nip05),
     69             (Some(disp), Some(username), None) => disp.union(username),
     70             (Some(disp), None, None) => disp,
     71             (None, Some(username), Some(nip05)) => username.union(nip05),
     72             (None, Some(username), None) => username,
     73             _ => ui.add(Label::new(RichText::new(name.name()))),
     74         };
     75 
     76         if add_placeholder_space {
     77             ui.add_space(16.0);
     78         }
     79 
     80         resp
     81     }
     82 }
     83 
     84 pub fn about_section_widget<'a>(profile: Option<&'a ProfileRecord<'a>>) -> impl egui::Widget + 'a {
     85     move |ui: &mut egui::Ui| {
     86         if let Some(about) = profile
     87             .map(|p| p.record().profile())
     88             .and_then(|p| p.and_then(|p| p.about()))
     89         {
     90             let resp = ui.label(about);
     91             ui.add_space(8.0);
     92             resp
     93         } else {
     94             // need any Response so we dont need an Option
     95             ui.allocate_response(egui::Vec2::ZERO, egui::Sense::hover())
     96         }
     97     }
     98 }
     99 
    100 /// Loads a banner texture using the shared media cache to prevent blocking.
    101 #[profiling::function]
    102 pub fn banner_texture<'a>(
    103     ui: &mut egui::Ui,
    104     cache: &'a mut Images,
    105     jobs: &MediaJobSender,
    106     banner_url: &str,
    107     size: PointDimensions,
    108 ) -> Option<&'a TextureHandle> {
    109     if banner_url.is_empty() {
    110         return None;
    111     }
    112 
    113     cache.latest_texture(
    114         jobs,
    115         ui,
    116         banner_url,
    117         ImageType::Content(Some(size.to_pixels(ui))),
    118         AnimationMode::NoAnimation,
    119     )
    120 }
    121 
    122 /// Renders a profile banner via the cached loader so we avoid egui_extras overhead.
    123 #[profiling::function]
    124 pub fn banner(
    125     ui: &mut egui::Ui,
    126     cache: &mut Images,
    127     jobs: &MediaJobSender,
    128     banner_url: Option<&str>,
    129     height: f32,
    130 ) -> egui::Response {
    131     let x = ui.available_size().x;
    132     ui.add_sized([x, height], |ui: &mut egui::Ui| {
    133         banner_url
    134             .and_then(|url| banner_texture(ui, cache, jobs, url, PointDimensions { x, y: height }))
    135             .map(|texture| {
    136                 let size = texture.size_vec2();
    137                 let aspect_ratio = if size.y == 0.0 { 1.0 } else { size.x / size.y };
    138 
    139                 notedeck::media::images::aspect_fill(
    140                     ui,
    141                     egui::Sense::hover(),
    142                     texture.id(),
    143                     aspect_ratio,
    144                 )
    145             })
    146             .unwrap_or_else(|| empty_banner(ui))
    147     })
    148 }
    149 
    150 /// Draws an empty banner placeholder while the image loads or is missing.
    151 fn empty_banner(ui: &mut egui::Ui) -> egui::Response {
    152     let (rect, response) = ui.allocate_exact_size(ui.available_size(), egui::Sense::hover());
    153     ui.painter()
    154         .rect_filled(rect, 0.0, ui.visuals().faint_bg_color);
    155     response
    156 }
    157 
    158 pub fn follow_button(following: IsFollowing) -> impl egui::Widget + 'static {
    159     move |ui: &mut egui::Ui| -> egui::Response {
    160         let (bg_color, text) = match following {
    161             IsFollowing::Unknown => (ui.visuals().noninteractive().bg_fill, "Unknown"),
    162             IsFollowing::Yes => (ui.visuals().widgets.inactive.bg_fill, "Unfollow"),
    163             IsFollowing::No => (colors::PINK, "Follow"),
    164         };
    165 
    166         let enabled = following != IsFollowing::Unknown;
    167         ui.add(styled_button_toggleable(text, bg_color, enabled))
    168     }
    169 }