notedeck

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

context.rs (5972B)


      1 use egui::{Rect, Vec2};
      2 use enostr::{NoteId, Pubkey};
      3 use nostrdb::{Note, NoteKey};
      4 use tracing::error;
      5 
      6 #[derive(Clone)]
      7 #[allow(clippy::enum_variant_names)]
      8 pub enum NoteContextSelection {
      9     CopyText,
     10     CopyPubkey,
     11     CopyNoteId,
     12     CopyNoteJSON,
     13 }
     14 
     15 impl NoteContextSelection {
     16     pub fn process(&self, ui: &mut egui::Ui, note: &Note<'_>) {
     17         match self {
     18             NoteContextSelection::CopyText => {
     19                 ui.output_mut(|w| {
     20                     w.copied_text = note.content().to_string();
     21                 });
     22             }
     23             NoteContextSelection::CopyPubkey => {
     24                 ui.output_mut(|w| {
     25                     if let Some(bech) = Pubkey::new(*note.pubkey()).to_bech() {
     26                         w.copied_text = bech;
     27                     }
     28                 });
     29             }
     30             NoteContextSelection::CopyNoteId => {
     31                 ui.output_mut(|w| {
     32                     if let Some(bech) = NoteId::new(*note.id()).to_bech() {
     33                         w.copied_text = bech;
     34                     }
     35                 });
     36             }
     37             NoteContextSelection::CopyNoteJSON => {
     38                 ui.output_mut(|w| match note.json() {
     39                     Ok(json) => w.copied_text = json,
     40                     Err(err) => error!("error copying note json: {err}"),
     41                 });
     42             }
     43         }
     44     }
     45 }
     46 
     47 pub struct NoteContextButton {
     48     put_at: Option<Rect>,
     49     note_key: NoteKey,
     50 }
     51 
     52 impl egui::Widget for NoteContextButton {
     53     fn ui(self, ui: &mut egui::Ui) -> egui::Response {
     54         let r = if let Some(r) = self.put_at {
     55             r
     56         } else {
     57             let mut place = ui.available_rect_before_wrap();
     58             let size = Self::max_width();
     59             place.set_width(size);
     60             place.set_height(size);
     61             place
     62         };
     63 
     64         Self::show(ui, self.note_key, r)
     65     }
     66 }
     67 
     68 impl NoteContextButton {
     69     pub fn new(note_key: NoteKey) -> Self {
     70         let put_at: Option<Rect> = None;
     71         NoteContextButton { note_key, put_at }
     72     }
     73 
     74     pub fn place_at(mut self, rect: Rect) -> Self {
     75         self.put_at = Some(rect);
     76         self
     77     }
     78 
     79     pub fn max_width() -> f32 {
     80         Self::max_radius() * 3.0 + Self::max_distance_between_circles() * 2.0
     81     }
     82 
     83     pub fn size() -> Vec2 {
     84         let width = Self::max_width();
     85         egui::vec2(width, width)
     86     }
     87 
     88     fn max_radius() -> f32 {
     89         8.0
     90     }
     91 
     92     fn min_radius() -> f32 {
     93         Self::max_radius() / Self::expansion_multiple()
     94     }
     95 
     96     fn max_distance_between_circles() -> f32 {
     97         2.0
     98     }
     99 
    100     fn expansion_multiple() -> f32 {
    101         2.0
    102     }
    103 
    104     fn min_distance_between_circles() -> f32 {
    105         Self::max_distance_between_circles() / Self::expansion_multiple()
    106     }
    107 
    108     pub fn show(ui: &mut egui::Ui, note_key: NoteKey, put_at: Rect) -> egui::Response {
    109         #[cfg(feature = "profiling")]
    110         puffin::profile_function!();
    111 
    112         let id = ui.id().with(("more_options_anim", note_key));
    113 
    114         let min_radius = Self::min_radius();
    115         let anim_speed = 0.05;
    116         let response = ui.interact(put_at, id, egui::Sense::click());
    117 
    118         let hovered = response.hovered();
    119         let animation_progress = ui.ctx().animate_bool_with_time(id, hovered, anim_speed);
    120 
    121         if hovered {
    122             ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand);
    123         }
    124 
    125         let min_distance = Self::min_distance_between_circles();
    126         let cur_distance = min_distance
    127             + (Self::max_distance_between_circles() - min_distance) * animation_progress;
    128 
    129         let cur_radius = min_radius + (Self::max_radius() - min_radius) * animation_progress;
    130 
    131         let center = put_at.center();
    132         let left_circle_center = center - egui::vec2(cur_distance + cur_radius, 0.0);
    133         let right_circle_center = center + egui::vec2(cur_distance + cur_radius, 0.0);
    134 
    135         let translated_radius = (cur_radius - 1.0) / 2.0;
    136 
    137         // This works in both themes
    138         let color = ui.style().visuals.noninteractive().fg_stroke.color;
    139 
    140         // Draw circles
    141         let painter = ui.painter_at(put_at);
    142         painter.circle_filled(left_circle_center, translated_radius, color);
    143         painter.circle_filled(center, translated_radius, color);
    144         painter.circle_filled(right_circle_center, translated_radius, color);
    145 
    146         response
    147     }
    148 
    149     pub fn menu(
    150         ui: &mut egui::Ui,
    151         button_response: egui::Response,
    152     ) -> Option<NoteContextSelection> {
    153         #[cfg(feature = "profiling")]
    154         puffin::profile_function!();
    155 
    156         let mut context_selection: Option<NoteContextSelection> = None;
    157 
    158         stationary_arbitrary_menu_button(ui, button_response, |ui| {
    159             ui.set_max_width(200.0);
    160             if ui.button("Copy text").clicked() {
    161                 context_selection = Some(NoteContextSelection::CopyText);
    162                 ui.close_menu();
    163             }
    164             if ui.button("Copy user public key").clicked() {
    165                 context_selection = Some(NoteContextSelection::CopyPubkey);
    166                 ui.close_menu();
    167             }
    168             if ui.button("Copy note id").clicked() {
    169                 context_selection = Some(NoteContextSelection::CopyNoteId);
    170                 ui.close_menu();
    171             }
    172             if ui.button("Copy note json").clicked() {
    173                 context_selection = Some(NoteContextSelection::CopyNoteJSON);
    174                 ui.close_menu();
    175             }
    176         });
    177 
    178         context_selection
    179     }
    180 }
    181 
    182 fn stationary_arbitrary_menu_button<R>(
    183     ui: &mut egui::Ui,
    184     button_response: egui::Response,
    185     add_contents: impl FnOnce(&mut egui::Ui) -> R,
    186 ) -> egui::InnerResponse<Option<R>> {
    187     let bar_id = ui.id();
    188     let mut bar_state = egui::menu::BarState::load(ui.ctx(), bar_id);
    189 
    190     let inner = bar_state.bar_menu(&button_response, add_contents);
    191 
    192     bar_state.store(ui.ctx(), bar_id);
    193     egui::InnerResponse::new(inner.map(|r| r.inner), button_response)
    194 }