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 }