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