notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

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 }