use egui::{vec2, FontId, Layout, Pos2, Rect, ScrollArea, UiBuilder, Vec2b}; use nostrdb::{Ndb, ProfileRecord, Transaction}; use notedeck::{ fonts::get_font_size, name::get_display_name, profile::get_profile_url, Images, NotedeckTextStyle, }; use notedeck_ui::{ anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE}, widgets::x_button, ProfilePic, }; use tracing::error; pub struct SearchResultsView<'a> { ndb: &'a Ndb, txn: &'a Transaction, img_cache: &'a mut Images, results: &'a Vec<&'a [u8; 32]>, } pub enum SearchResultsResponse { SelectResult(Option), DeleteMention, } impl<'a> SearchResultsView<'a> { pub fn new( img_cache: &'a mut Images, ndb: &'a Ndb, txn: &'a Transaction, results: &'a Vec<&'a [u8; 32]>, ) -> Self { Self { ndb, txn, img_cache, results, } } fn show(&mut self, ui: &mut egui::Ui, width: f32) -> SearchResultsResponse { let mut search_results_selection = None; ui.vertical(|ui| { for (i, res) in self.results.iter().enumerate() { let profile = match self.ndb.get_profile_by_pubkey(self.txn, res) { Ok(rec) => rec, Err(e) => { error!("Error fetching profile for pubkey {:?}: {e}", res); return; } }; if ui .add(user_result(&profile, self.img_cache, i, width)) .clicked() { search_results_selection = Some(i) } } }); SearchResultsResponse::SelectResult(search_results_selection) } pub fn show_in_rect(&mut self, rect: egui::Rect, ui: &mut egui::Ui) -> SearchResultsResponse { let widget_id = ui.id().with("search_results"); let area_resp = egui::Area::new(widget_id) .order(egui::Order::Foreground) .fixed_pos(rect.left_top()) .constrain_to(rect) .show(ui.ctx(), |ui| { let inner_margin_size = 8.0; egui::Frame::NONE .fill(ui.visuals().panel_fill) .inner_margin(inner_margin_size) .show(ui, |ui| { let width = rect.width() - (2.0 * inner_margin_size); let close_button_resp = { let close_button_size = 16.0; let (close_section_rect, _) = ui.allocate_exact_size( vec2(width, close_button_size), egui::Sense::hover(), ); let (_, button_rect) = close_section_rect.split_left_right_at_x( close_section_rect.right() - close_button_size, ); let button_resp = ui.allocate_rect(button_rect, egui::Sense::click()); ui.allocate_new_ui( UiBuilder::new() .max_rect(close_section_rect) .layout(Layout::right_to_left(egui::Align::Center)), |ui| ui.add(x_button(button_resp.rect)).clicked(), ) .inner }; ui.add_space(8.0); let scroll_resp = ScrollArea::vertical() .max_width(width) .auto_shrink(Vec2b::FALSE) .show(ui, |ui| self.show(ui, width)); ui.advance_cursor_after_rect(rect); if close_button_resp { SearchResultsResponse::DeleteMention } else { scroll_resp.inner } }) .inner }); area_resp.inner } } fn user_result<'a>( profile: &'a ProfileRecord<'_>, cache: &'a mut Images, index: usize, width: f32, ) -> impl egui::Widget + 'a { move |ui: &mut egui::Ui| -> egui::Response { let min_img_size = 48.0; let max_image = min_img_size * ICON_EXPANSION_MULTIPLE; let spacing = 8.0; let body_font_size = get_font_size(ui.ctx(), &NotedeckTextStyle::Body); let helper = AnimationHelper::new(ui, ("user_result", index), vec2(width, max_image)); let icon_rect = { let r = helper.get_animation_rect(); let mut center = r.center(); center.x = r.left() + (max_image / 2.0); let size = helper.scale_1d_pos(min_img_size); Rect::from_center_size(center, vec2(size, size)) }; let pfp_resp = ui.put( icon_rect, ProfilePic::new(cache, get_profile_url(Some(profile))) .size(helper.scale_1d_pos(min_img_size)), ); let name_font = FontId::new( helper.scale_1d_pos(body_font_size), NotedeckTextStyle::Body.font_family(), ); let painter = ui.painter_at(helper.get_animation_rect()); let name_galley = painter.layout( get_display_name(Some(profile)).name().to_owned(), name_font, ui.visuals().text_color(), width, ); let galley_pos = { let right_top = pfp_resp.rect.right_top(); let galley_pos_y = pfp_resp.rect.center().y - (name_galley.rect.height() / 2.0); Pos2::new(right_top.x + spacing, galley_pos_y) }; painter.galley(galley_pos, name_galley, ui.visuals().text_color()); ui.advance_cursor_after_rect(helper.get_animation_rect()); pfp_resp.union(helper.take_animation_response()) } }