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 }