notedeck

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

mentions_picker.rs (6928B)


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