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