mod.rs (7448B)
1 use egui::{vec2, Align, Color32, RichText, Rounding, Stroke, TextEdit}; 2 3 use super::{note::contents::NoteContext, padding}; 4 use crate::{ 5 actionbar::NoteAction, 6 ui::{note::NoteOptions, timeline::TimelineTabView}, 7 }; 8 use nostrdb::{Filter, Transaction}; 9 use notedeck::{MuteFun, NoteRef}; 10 use std::time::{Duration, Instant}; 11 use tracing::{error, info, warn}; 12 13 mod state; 14 15 pub use state::{FocusState, SearchQueryState, SearchState}; 16 17 pub struct SearchView<'a, 'd> { 18 query: &'a mut SearchQueryState, 19 note_options: NoteOptions, 20 txn: &'a Transaction, 21 is_muted: &'a MuteFun, 22 note_context: &'a mut NoteContext<'d>, 23 } 24 25 impl<'a, 'd> SearchView<'a, 'd> { 26 pub fn new( 27 txn: &'a Transaction, 28 is_muted: &'a MuteFun, 29 note_options: NoteOptions, 30 query: &'a mut SearchQueryState, 31 note_context: &'a mut NoteContext<'d>, 32 ) -> Self { 33 Self { 34 txn, 35 is_muted, 36 query, 37 note_options, 38 note_context, 39 } 40 } 41 42 pub fn show(&mut self, ui: &mut egui::Ui) -> Option<NoteAction> { 43 padding(8.0, ui, |ui| self.show_impl(ui)).inner 44 } 45 46 pub fn show_impl(&mut self, ui: &mut egui::Ui) -> Option<NoteAction> { 47 ui.spacing_mut().item_spacing = egui::vec2(0.0, 12.0); 48 49 if search_box(self.query, ui) { 50 self.execute_search(ui.ctx()); 51 } 52 53 match self.query.state { 54 SearchState::New | SearchState::Navigating => None, 55 56 SearchState::Searched | SearchState::Typing => { 57 if self.query.state == SearchState::Typing { 58 ui.label(format!("Searching for '{}'", &self.query.string)); 59 } else { 60 ui.label(format!( 61 "Got {} results for '{}'", 62 self.query.notes.notes.len(), 63 &self.query.string 64 )); 65 } 66 67 egui::ScrollArea::vertical() 68 .show(ui, |ui| { 69 let reversed = false; 70 TimelineTabView::new( 71 &self.query.notes, 72 reversed, 73 self.note_options, 74 self.txn, 75 self.is_muted, 76 self.note_context, 77 ) 78 .show(ui) 79 }) 80 .inner 81 } 82 } 83 } 84 85 fn execute_search(&mut self, ctx: &egui::Context) { 86 if self.query.string.is_empty() { 87 return; 88 } 89 90 let max_results = 500; 91 let filter = Filter::new() 92 .search(&self.query.string) 93 .kinds([1]) 94 .limit(max_results) 95 .build(); 96 97 // TODO: execute in thread 98 99 let before = Instant::now(); 100 let qrs = self 101 .note_context 102 .ndb 103 .query(self.txn, &[filter], max_results as i32); 104 let after = Instant::now(); 105 let duration = after - before; 106 107 if duration > Duration::from_millis(20) { 108 warn!( 109 "query took {:?}... let's update this to use a thread!", 110 after - before 111 ); 112 } 113 114 match qrs { 115 Ok(qrs) => { 116 info!( 117 "queried '{}' and got {} results", 118 self.query.string, 119 qrs.len() 120 ); 121 122 let note_refs = qrs.into_iter().map(NoteRef::from_query_result).collect(); 123 self.query.notes.notes = note_refs; 124 self.query.notes.list.borrow_mut().reset(); 125 ctx.request_repaint(); 126 } 127 128 Err(err) => { 129 error!("fulltext query failed: {err}") 130 } 131 } 132 } 133 } 134 135 fn search_box(query: &mut SearchQueryState, ui: &mut egui::Ui) -> bool { 136 ui.horizontal(|ui| { 137 // Container for search input and icon 138 let search_container = egui::Frame { 139 inner_margin: egui::Margin::symmetric(8, 0), 140 outer_margin: egui::Margin::ZERO, 141 rounding: Rounding::same(18), // More rounded corners 142 shadow: Default::default(), 143 fill: Color32::from_rgb(30, 30, 30), // Darker background to match screenshot 144 stroke: Stroke::new(1.0, Color32::from_rgb(60, 60, 60)), 145 }; 146 147 search_container 148 .show(ui, |ui| { 149 // Use layout to align items vertically centered 150 ui.with_layout(egui::Layout::left_to_right(Align::Center), |ui| { 151 ui.spacing_mut().item_spacing = egui::vec2(8.0, 0.0); 152 153 let search_height = 34.0; 154 // Magnifying glass icon 155 ui.add(search_icon(16.0, search_height)); 156 157 let before_len = query.string.len(); 158 159 // Search input field 160 //let font_size = notedeck::fonts::get_font_size(ui.ctx(), &NotedeckTextStyle::Body); 161 let response = ui.add_sized( 162 [ui.available_width(), search_height], 163 TextEdit::singleline(&mut query.string) 164 .hint_text(RichText::new("Search notes...").weak()) 165 //.desired_width(available_width - 32.0) 166 //.font(egui::FontId::new(font_size, egui::FontFamily::Proportional)) 167 .margin(vec2(0.0, 8.0)) 168 .frame(false), 169 ); 170 171 if query.focus_state == FocusState::ShouldRequestFocus { 172 response.request_focus(); 173 query.focus_state = FocusState::RequestedFocus; 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 }