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 }