mod.rs (7316B)
1 use egui::{vec2, Align, Color32, RichText, Rounding, Stroke, TextEdit}; 2 3 use super::padding; 4 use crate::{ 5 actionbar::NoteAction, 6 ui::{note::NoteOptions, timeline::TimelineTabView}, 7 }; 8 use nostrdb::{Filter, Ndb, Transaction}; 9 use notedeck::{Images, MuteFun, NoteCache, NoteRef}; 10 use std::time::{Duration, Instant}; 11 use tracing::{error, info, warn}; 12 13 mod state; 14 15 pub use state::{SearchQueryState, SearchState}; 16 17 pub struct SearchView<'a> { 18 query: &'a mut SearchQueryState, 19 ndb: &'a Ndb, 20 note_options: NoteOptions, 21 txn: &'a Transaction, 22 note_cache: &'a mut NoteCache, 23 img_cache: &'a mut Images, 24 is_muted: &'a MuteFun, 25 } 26 27 impl<'a> SearchView<'a> { 28 pub fn new( 29 ndb: &'a Ndb, 30 txn: &'a Transaction, 31 note_cache: &'a mut NoteCache, 32 img_cache: &'a mut Images, 33 is_muted: &'a MuteFun, 34 note_options: NoteOptions, 35 query: &'a mut SearchQueryState, 36 ) -> Self { 37 Self { 38 ndb, 39 txn, 40 note_cache, 41 img_cache, 42 is_muted, 43 query, 44 note_options, 45 } 46 } 47 48 pub fn show(&mut self, ui: &mut egui::Ui) -> Option<NoteAction> { 49 padding(8.0, ui, |ui| self.show_impl(ui)).inner 50 } 51 52 pub fn show_impl(&mut self, ui: &mut egui::Ui) -> Option<NoteAction> { 53 ui.spacing_mut().item_spacing = egui::vec2(0.0, 12.0); 54 55 if search_box(self.query, ui) { 56 self.execute_search(ui.ctx()); 57 } 58 59 match self.query.state { 60 SearchState::New => None, 61 62 SearchState::Searched | SearchState::Typing => { 63 if self.query.state == SearchState::Typing { 64 ui.label(format!("Searching for '{}'", &self.query.string)); 65 } else { 66 ui.label(format!( 67 "Got {} results for '{}'", 68 self.query.notes.notes.len(), 69 &self.query.string 70 )); 71 } 72 73 egui::ScrollArea::vertical() 74 .show(ui, |ui| { 75 let reversed = false; 76 TimelineTabView::new( 77 &self.query.notes, 78 reversed, 79 self.note_options, 80 self.txn, 81 self.ndb, 82 self.note_cache, 83 self.img_cache, 84 self.is_muted, 85 ) 86 .show(ui) 87 }) 88 .inner 89 } 90 } 91 } 92 93 fn execute_search(&mut self, ctx: &egui::Context) { 94 if self.query.string.is_empty() { 95 return; 96 } 97 98 let max_results = 500; 99 let filter = Filter::new() 100 .search(&self.query.string) 101 .kinds([1]) 102 .limit(max_results) 103 .build(); 104 105 // TODO: execute in thread 106 107 let before = Instant::now(); 108 let qrs = self.ndb.query(self.txn, &[filter], max_results as i32); 109 let after = Instant::now(); 110 let duration = after - before; 111 112 if duration > Duration::from_millis(20) { 113 warn!( 114 "query took {:?}... let's update this to use a thread!", 115 after - before 116 ); 117 } 118 119 match qrs { 120 Ok(qrs) => { 121 info!( 122 "queried '{}' and got {} results", 123 self.query.string, 124 qrs.len() 125 ); 126 127 let note_refs = qrs.into_iter().map(NoteRef::from_query_result).collect(); 128 self.query.notes.notes = note_refs; 129 self.query.notes.list.borrow_mut().reset(); 130 ctx.request_repaint(); 131 } 132 133 Err(err) => { 134 error!("fulltext query failed: {err}") 135 } 136 } 137 } 138 } 139 140 fn search_box(query: &mut SearchQueryState, ui: &mut egui::Ui) -> bool { 141 ui.horizontal(|ui| { 142 // Container for search input and icon 143 let search_container = egui::Frame { 144 inner_margin: egui::Margin::symmetric(8.0, 0.0), 145 outer_margin: egui::Margin::ZERO, 146 rounding: Rounding::same(18.0), // More rounded corners 147 shadow: Default::default(), 148 fill: Color32::from_rgb(30, 30, 30), // Darker background to match screenshot 149 stroke: Stroke::new(1.0, Color32::from_rgb(60, 60, 60)), 150 }; 151 152 search_container 153 .show(ui, |ui| { 154 // Use layout to align items vertically centered 155 ui.with_layout(egui::Layout::left_to_right(Align::Center), |ui| { 156 ui.spacing_mut().item_spacing = egui::vec2(8.0, 0.0); 157 158 let search_height = 34.0; 159 // Magnifying glass icon 160 ui.add(search_icon(16.0, search_height)); 161 162 let before_len = query.string.len(); 163 164 // Search input field 165 //let font_size = notedeck::fonts::get_font_size(ui.ctx(), &NotedeckTextStyle::Body); 166 ui.add_sized( 167 [ui.available_width(), search_height], 168 TextEdit::singleline(&mut query.string) 169 .hint_text(RichText::new("Search notes...").weak()) 170 //.desired_width(available_width - 32.0) 171 //.font(egui::FontId::new(font_size, egui::FontFamily::Proportional)) 172 .margin(vec2(0.0, 8.0)) 173 .frame(false), 174 ); 175 176 let after_len = query.string.len(); 177 178 let changed = before_len != after_len; 179 if changed { 180 query.mark_updated(); 181 } 182 183 // Execute search after debouncing 184 if query.should_search() { 185 query.mark_searched(SearchState::Searched); 186 true 187 } else { 188 false 189 } 190 }) 191 .inner 192 }) 193 .inner 194 }) 195 .inner 196 } 197 198 /// Creates a magnifying glass icon widget 199 fn search_icon(size: f32, height: f32) -> impl egui::Widget { 200 move |ui: &mut egui::Ui| { 201 // Use the provided height parameter 202 let desired_size = vec2(size, height); 203 let (rect, response) = ui.allocate_exact_size(desired_size, egui::Sense::hover()); 204 205 // Calculate center position - this ensures the icon is centered in its allocated space 206 let center_pos = rect.center(); 207 let stroke = Stroke::new(1.5, Color32::from_rgb(150, 150, 150)); 208 209 // Draw circle 210 let circle_radius = size * 0.35; 211 ui.painter() 212 .circle(center_pos, circle_radius, Color32::TRANSPARENT, stroke); 213 214 // Draw handle 215 let handle_start = center_pos + vec2(circle_radius * 0.7, circle_radius * 0.7); 216 let handle_end = handle_start + vec2(size * 0.25, size * 0.25); 217 ui.painter() 218 .line_segment([handle_start, handle_end], stroke); 219 220 response 221 } 222 }