notedeck

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

context.rs (5685B)


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