mentions_picker.rs (6928B)
1 use egui::{vec2, FontId, Layout, Pos2, Rect, ScrollArea, UiBuilder, Vec2b}; 2 use nostrdb::{Ndb, ProfileRecord, Transaction}; 3 use notedeck::{ 4 fonts::get_font_size, name::get_display_name, profile::get_profile_url, DragResponse, Images, 5 MediaJobSender, NotedeckTextStyle, 6 }; 7 use notedeck_ui::{ 8 anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE}, 9 widgets::x_button, 10 ProfilePic, 11 }; 12 use tracing::error; 13 14 /// Displays user profiles for the user to pick from. 15 /// Useful for manually typing a username and selecting the profile desired 16 pub struct MentionPickerView<'a> { 17 ndb: &'a Ndb, 18 txn: &'a Transaction, 19 img_cache: &'a mut Images, 20 results: &'a Vec<&'a [u8; 32]>, 21 jobs: &'a MediaJobSender, 22 } 23 24 pub enum MentionPickerResponse { 25 SelectResult(Option<usize>), 26 DeleteMention, 27 } 28 29 impl<'a> MentionPickerView<'a> { 30 pub fn new( 31 img_cache: &'a mut Images, 32 ndb: &'a Ndb, 33 txn: &'a Transaction, 34 results: &'a Vec<&'a [u8; 32]>, 35 jobs: &'a MediaJobSender, 36 ) -> Self { 37 Self { 38 ndb, 39 txn, 40 img_cache, 41 results, 42 jobs, 43 } 44 } 45 46 fn show(&mut self, ui: &mut egui::Ui, width: f32) -> MentionPickerResponse { 47 let mut selection = None; 48 ui.vertical(|ui| { 49 for (i, res) in self.results.iter().enumerate() { 50 let profile = match self.ndb.get_profile_by_pubkey(self.txn, res) { 51 Ok(rec) => rec, 52 Err(e) => { 53 error!("Error fetching profile for pubkey {:?}: {e}", res); 54 return; 55 } 56 }; 57 58 if ui 59 .add(user_result(&profile, self.img_cache, self.jobs, i, width)) 60 .clicked() 61 { 62 selection = Some(i) 63 } 64 } 65 }); 66 67 MentionPickerResponse::SelectResult(selection) 68 } 69 70 pub fn show_in_rect( 71 &mut self, 72 rect: egui::Rect, 73 ui: &mut egui::Ui, 74 ) -> DragResponse<MentionPickerResponse> { 75 let widget_id = ui.id().with("mention_results"); 76 let area_resp = egui::Area::new(widget_id) 77 .order(egui::Order::Foreground) 78 .fixed_pos(rect.left_top()) 79 .constrain_to(rect) 80 .show(ui.ctx(), |ui| { 81 let inner_margin_size = 8.0; 82 egui::Frame::NONE 83 .fill(ui.visuals().panel_fill) 84 .show(ui, |ui| { 85 let width = rect.width() - (2.0 * inner_margin_size); 86 87 ui.allocate_space(vec2(ui.available_width(), inner_margin_size)); 88 let close_button_resp = { 89 let close_button_size = 16.0; 90 let (close_section_rect, _) = ui.allocate_exact_size( 91 vec2(width, close_button_size), 92 egui::Sense::hover(), 93 ); 94 let (_, button_rect) = close_section_rect.split_left_right_at_x( 95 close_section_rect.right() - close_button_size, 96 ); 97 let button_resp = ui.allocate_rect(button_rect, egui::Sense::click()); 98 ui.allocate_new_ui( 99 UiBuilder::new() 100 .max_rect(close_section_rect) 101 .layout(Layout::right_to_left(egui::Align::Center)), 102 |ui| ui.add(x_button(button_resp.rect)).clicked(), 103 ) 104 .inner 105 }; 106 107 ui.allocate_space(vec2(ui.available_width(), inner_margin_size)); 108 109 let scroll_resp = ScrollArea::vertical() 110 .max_width(rect.width()) 111 .auto_shrink(Vec2b::FALSE) 112 .show(ui, |ui| Some(self.show(ui, width))); 113 ui.advance_cursor_after_rect(rect); 114 115 DragResponse::scroll(scroll_resp).map_output(|o| { 116 if close_button_resp { 117 MentionPickerResponse::DeleteMention 118 } else { 119 o 120 } 121 }) 122 }) 123 .inner 124 }); 125 126 area_resp.inner 127 } 128 } 129 130 fn user_result<'a>( 131 profile: &'a ProfileRecord<'_>, 132 cache: &'a mut Images, 133 jobs: &'a MediaJobSender, 134 index: usize, 135 width: f32, 136 ) -> impl egui::Widget + 'a { 137 move |ui: &mut egui::Ui| -> egui::Response { 138 let min_img_size = 48.0; 139 let max_image = min_img_size * ICON_EXPANSION_MULTIPLE; 140 let spacing = 8.0; 141 let body_font_size = get_font_size(ui.ctx(), &NotedeckTextStyle::Body); 142 143 let animation_rect = { 144 let max_width = ui.available_width(); 145 let extra_width = (max_width - width) / 2.0; 146 let left = ui.cursor().left(); 147 let (rect, _) = 148 ui.allocate_exact_size(vec2(width + extra_width, max_image), egui::Sense::click()); 149 150 let (_, right) = rect.split_left_right_at_x(left + extra_width); 151 right 152 }; 153 154 let helper = AnimationHelper::new_from_rect(ui, ("user_result", index), animation_rect); 155 156 let icon_rect = { 157 let r = helper.get_animation_rect(); 158 let mut center = r.center(); 159 center.x = r.left() + (max_image / 2.0); 160 let size = helper.scale_1d_pos(min_img_size); 161 Rect::from_center_size(center, vec2(size, size)) 162 }; 163 164 let pfp_resp = ui.put( 165 icon_rect, 166 &mut ProfilePic::new(cache, jobs, get_profile_url(Some(profile))) 167 .size(helper.scale_1d_pos(min_img_size)), 168 ); 169 170 let name_font = FontId::new( 171 helper.scale_1d_pos(body_font_size), 172 NotedeckTextStyle::Body.font_family(), 173 ); 174 let painter = ui.painter_at(helper.get_animation_rect()); 175 let name_galley = painter.layout( 176 get_display_name(Some(profile)).name().to_owned(), 177 name_font, 178 ui.visuals().text_color(), 179 width, 180 ); 181 182 let galley_pos = { 183 let right_top = pfp_resp.rect.right_top(); 184 let galley_pos_y = pfp_resp.rect.center().y - (name_galley.rect.height() / 2.0); 185 Pos2::new(right_top.x + spacing, galley_pos_y) 186 }; 187 188 painter.galley(galley_pos, name_galley, ui.visuals().text_color()); 189 ui.advance_cursor_after_rect(helper.get_animation_rect()); 190 191 pfp_resp.union(helper.take_animation_response()) 192 } 193 }