From 6f5f090fbe0ad1fa59b326e67e8e026f08fea9b1 Mon Sep 17 00:00:00 2001 From: kernelkind Date: Thu, 19 Sep 2024 18:20:17 -0400 Subject: [PATCH 1/9] Add 'more options' to each note Signed-off-by: kernelkind --- enostr/src/pubkey.rs | 4 ++ src/lib.rs | 1 + src/note_options.rs | 40 +++++++++++ src/notecache.rs | 8 +-- src/ui/note/contents.rs | 1 + src/ui/note/mod.rs | 149 +++++++++++++++++++++++++++++++++++----- src/ui/note/reply.rs | 1 + src/ui/thread.rs | 13 ++-- src/ui/timeline.rs | 4 ++ 9 files changed, 193 insertions(+), 28 deletions(-) create mode 100644 src/note_options.rs diff --git a/enostr/src/pubkey.rs b/enostr/src/pubkey.rs index a46c75f..ad8c849 100644 --- a/enostr/src/pubkey.rs +++ b/enostr/src/pubkey.rs @@ -68,6 +68,10 @@ impl Pubkey { Ok(Pubkey(data.1.try_into().unwrap())) } } + + pub fn to_bech(&self) -> Option { + nostr::bech32::encode::(HRP_NPUB, &self.0).ok() + } } impl fmt::Display for Pubkey { diff --git a/src/lib.rs b/src/lib.rs index 22610e4..676d87e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,6 +23,7 @@ pub mod login_manager; mod macos_key_storage; mod nav; mod note; +mod note_options; mod notecache; mod post; mod post_action_executor; diff --git a/src/note_options.rs b/src/note_options.rs new file mode 100644 index 0000000..580253c --- /dev/null +++ b/src/note_options.rs @@ -0,0 +1,40 @@ +use enostr::{NoteId, Pubkey}; +use nostrdb::Note; + +#[derive(Clone)] +#[allow(clippy::enum_variant_names)] +pub enum NoteOptionSelection { + CopyText, + CopyPubkey, + CopyNoteId, +} + +pub fn process_note_selection( + ui: &mut egui::Ui, + selection: Option, + note: &Note<'_>, +) { + if let Some(option) = selection { + match option { + NoteOptionSelection::CopyText => { + ui.output_mut(|w| { + w.copied_text = note.content().to_string(); + }); + } + NoteOptionSelection::CopyPubkey => { + ui.output_mut(|w| { + if let Some(bech) = Pubkey::new(*note.pubkey()).to_bech() { + w.copied_text = bech; + } + }); + } + NoteOptionSelection::CopyNoteId => { + ui.output_mut(|w| { + if let Some(bech) = NoteId::new(*note.id()).to_bech() { + w.copied_text = bech; + } + }); + } + } + } +} diff --git a/src/notecache.rs b/src/notecache.rs index 82c1798..51bdc75 100644 --- a/src/notecache.rs +++ b/src/notecache.rs @@ -35,7 +35,6 @@ impl NoteCache { pub struct CachedNote { reltime: TimeCached, pub reply: NoteReplyBuf, - pub bar_open: bool, } impl CachedNote { @@ -46,12 +45,7 @@ impl CachedNote { Box::new(move || time_ago_since(created_at)), ); let reply = NoteReply::new(note.tags()).to_owned(); - let bar_open = false; - CachedNote { - reltime, - reply, - bar_open, - } + CachedNote { reltime, reply } } pub fn reltime_str_mut(&mut self) -> &str { diff --git a/src/ui/note/contents.rs b/src/ui/note/contents.rs index 21eb115..e48aa48 100644 --- a/src/ui/note/contents.rs +++ b/src/ui/note/contents.rs @@ -108,6 +108,7 @@ pub fn render_note_preview( .small_pfp(true) .wide(true) .note_previews(false) + .use_more_options_button(true) .show(ui); }) .response diff --git a/src/ui/note/mod.rs b/src/ui/note/mod.rs index 491905b..62509a7 100644 --- a/src/ui/note/mod.rs +++ b/src/ui/note/mod.rs @@ -15,10 +15,11 @@ use crate::{ app_style::NotedeckTextStyle, colors, imgcache::ImageCache, + note_options::NoteOptionSelection, notecache::{CachedNote, NoteCache}, ui::{self, View}, }; -use egui::{Id, Label, Response, RichText, Sense}; +use egui::{Align, Id, Label, Layout, Response, RichText, Sense}; use enostr::NoteId; use nostrdb::{Ndb, Note, NoteKey, NoteReply, Transaction}; @@ -30,11 +31,34 @@ pub struct NoteView<'a> { img_cache: &'a mut ImageCache, note: &'a nostrdb::Note<'a>, flags: NoteOptions, + use_options: bool, } pub struct NoteResponse { pub response: egui::Response, pub action: Option, + pub option_selection: Option, +} + +impl NoteResponse { + pub fn new(response: egui::Response) -> Self { + Self { + response, + action: None, + option_selection: None, + } + } + + pub fn with_action(self, action: Option) -> Self { + Self { action, ..self } + } + + pub fn select_option(self, option_selection: Option) -> Self { + Self { + option_selection, + ..self + } + } } impl<'a> View for NoteView<'a> { @@ -177,6 +201,7 @@ impl<'a> NoteView<'a> { img_cache, note, flags, + use_options: false, } } @@ -215,6 +240,13 @@ impl<'a> NoteView<'a> { self } + pub fn use_more_options_button(self, enable: bool) -> Self { + Self { + use_options: enable, + ..self + } + } + pub fn options(&self) -> NoteOptions { self.flags } @@ -324,10 +356,7 @@ impl<'a> NoteView<'a> { pub fn show(&mut self, ui: &mut egui::Ui) -> NoteResponse { if self.options().has_textmode() { - NoteResponse { - response: self.textmode_ui(ui), - action: None, - } + NoteResponse::new(self.textmode_ui(ui)) } else { let txn = self.note.txn().expect("txn"); if let Some(note_to_repost) = get_reposted_note(self.ndb, txn, self.note) { @@ -369,17 +398,29 @@ impl<'a> NoteView<'a> { note_cache: &mut NoteCache, note: &Note, profile: &Result, nostrdb::Error>, - ) -> egui::Response { + use_options_button: bool, + ) -> NoteResponse { let note_key = note.key().unwrap(); - ui.horizontal(|ui| { + let inner_response = ui.horizontal(|ui| { ui.spacing_mut().item_spacing.x = 2.0; ui.add(ui::Username::new(profile.as_ref().ok(), note.pubkey()).abbreviated(20)); let cached_note = note_cache.cached_note_or_insert_mut(note_key, note); render_reltime(ui, cached_note, true); - }) - .response + + if use_options_button { + ui.with_layout(Layout::right_to_left(Align::Center), |ui| { + let more_options_resp = more_options_button(ui, note_key, 8.0); + options_context_menu(more_options_resp) + }) + .inner + } else { + None + } + }); + + NoteResponse::new(inner_response.response).select_option(inner_response.inner) } fn show_standard(&mut self, ui: &mut egui::Ui) -> NoteResponse { @@ -388,6 +429,7 @@ impl<'a> NoteView<'a> { let note_key = self.note.key().expect("todo: support non-db notes"); let txn = self.note.txn().expect("todo: support non-db notes"); let mut note_action: Option = None; + let mut selected_option: Option = None; let profile = self.ndb.get_profile_by_pubkey(txn, self.note.pubkey()); let maybe_hitbox = maybe_note_hitbox(ui, note_key); @@ -400,7 +442,14 @@ impl<'a> NoteView<'a> { ui.vertical(|ui| { ui.add_sized([size.x, self.options().pfp_size()], |ui: &mut egui::Ui| { ui.horizontal_centered(|ui| { - NoteView::note_header(ui, self.note_cache, self.note, &profile); + selected_option = NoteView::note_header( + ui, + self.note_cache, + self.note, + &profile, + self.use_options, + ) + .option_selection; }) .response }); @@ -440,8 +489,14 @@ impl<'a> NoteView<'a> { self.pfp(note_key, &profile, ui); ui.with_layout(egui::Layout::top_down(egui::Align::LEFT), |ui| { - NoteView::note_header(ui, self.note_cache, self.note, &profile); - + selected_option = NoteView::note_header( + ui, + self.note_cache, + self.note, + &profile, + self.use_options, + ) + .option_selection; ui.horizontal(|ui| { ui.spacing_mut().item_spacing.x = 2.0; @@ -483,10 +538,9 @@ impl<'a> NoteView<'a> { note_action, ); - NoteResponse { - response, - action: note_action, - } + NoteResponse::new(response) + .with_action(note_action) + .select_option(selected_option) } } @@ -631,3 +685,66 @@ fn quote_repost_button(ui: &mut egui::Ui, note_key: NoteKey) -> egui::Response { resp.union(put_resp) } + +fn more_options_button(ui: &mut egui::Ui, note_key: NoteKey, max_height: f32) -> egui::Response { + let id = ui.id().with(("more_options_anim", note_key)); + + let expansion_multiple = 2.0; + let max_radius = max_height; + let min_radius = max_radius / expansion_multiple; + let max_distance_between_circles = 2.0; + let min_distance_between_circles = max_distance_between_circles / expansion_multiple; + let max_width = max_radius * 3.0 + max_distance_between_circles * 2.0; + + let anim_speed = 0.05; + let expanded_size = egui::vec2(max_width, max_height); + let (rect, response) = ui.allocate_exact_size(expanded_size, egui::Sense::click()); + + let animation_progress = ui + .ctx() + .animate_bool_with_time(id, response.hovered(), anim_speed); + let cur_distance = min_distance_between_circles + + (max_distance_between_circles - min_distance_between_circles) * animation_progress; + let cur_radius = min_radius + (max_radius - min_radius) * animation_progress; + + let center = rect.center(); + let left_circle_center = center - egui::vec2(cur_distance + cur_radius, 0.0); + let right_circle_center = center + egui::vec2(cur_distance + cur_radius, 0.0); + + let translated_radius = (cur_radius - 1.0) / 2.0; + + let color = if ui.style().visuals.dark_mode { + egui::Color32::WHITE + } else { + egui::Color32::BLACK + }; + + // Draw circles + let painter = ui.painter_at(rect); + painter.circle_filled(left_circle_center, translated_radius, color); + painter.circle_filled(center, translated_radius, color); + painter.circle_filled(right_circle_center, translated_radius, color); + + response +} + +fn options_context_menu(more_options_button_resp: egui::Response) -> Option { + let mut selected_option: Option = None; + + more_options_button_resp.context_menu(|ui| { + ui.set_max_width(200.0); + if ui.button("Copy text").clicked() { + selected_option = Some(NoteOptionSelection::CopyText); + ui.close_menu(); + } + if ui.button("Copy user public key").clicked() { + selected_option = Some(NoteOptionSelection::CopyPubkey); + ui.close_menu(); + } + if ui.button("Copy note id").clicked() { + selected_option = Some(NoteOptionSelection::CopyNoteId); + ui.close_menu(); + } + }); + selected_option +} diff --git a/src/ui/note/reply.rs b/src/ui/note/reply.rs index a46ac5e..c2ed606 100644 --- a/src/ui/note/reply.rs +++ b/src/ui/note/reply.rs @@ -67,6 +67,7 @@ impl<'a> PostReplyView<'a> { ui::NoteView::new(self.ndb, self.note_cache, self.img_cache, self.note) .actionbar(false) .medium_pfp(true) + .use_more_options_button(true) .show(ui); }); diff --git a/src/ui/thread.rs b/src/ui/thread.rs index 0abf67f..3f67f04 100644 --- a/src/ui/thread.rs +++ b/src/ui/thread.rs @@ -1,5 +1,6 @@ use crate::{ - actionbar::BarAction, imgcache::ImageCache, notecache::NoteCache, thread::Threads, ui, + actionbar::BarAction, imgcache::ImageCache, note_options::process_note_selection, + notecache::NoteCache, thread::Threads, ui, }; use nostrdb::{Ndb, NoteKey, Transaction}; use tracing::{error, warn}; @@ -115,15 +116,17 @@ impl<'a> ThreadView<'a> { }; ui::padding(8.0, ui, |ui| { - if let Some(bar_action) = + let note_response = ui::NoteView::new(self.ndb, self.note_cache, self.img_cache, ¬e) .note_previews(!self.textmode) .textmode(self.textmode) - .show(ui) - .action - { + .use_more_options_button(!self.textmode) + .show(ui); + if let Some(bar_action) = note_response.action { action = Some(bar_action); } + + process_note_selection(ui, note_response.option_selection, ¬e); }); ui::hline(ui); diff --git a/src/ui/timeline.rs b/src/ui/timeline.rs index bc1e02b..a9e112e 100644 --- a/src/ui/timeline.rs +++ b/src/ui/timeline.rs @@ -1,4 +1,5 @@ use crate::draft::Draft; +use crate::note_options::process_note_selection; use crate::{ actionbar::BarAction, column::Columns, imgcache::ImageCache, notecache::NoteCache, timeline::TimelineId, ui, @@ -149,6 +150,7 @@ fn timeline_ui( let resp = ui::NoteView::new(ndb, note_cache, img_cache, ¬e) .note_previews(!textmode) .selectable_text(false) + .use_more_options_button(true) .show(ui); if let Some(ba) = resp.action { @@ -156,6 +158,8 @@ fn timeline_ui( } else if resp.response.clicked() { debug!("clicked note"); } + + process_note_selection(ui, resp.option_selection, ¬e); }); ui::hline(ui); From ad319b643f58e44d5cb0aff9ba5888949504d160 Mon Sep 17 00:00:00 2001 From: kernelkind Date: Thu, 19 Sep 2024 18:58:14 -0400 Subject: [PATCH 2/9] can left click note more options button egui doesn't support custom buttons so `stationary_arbitrary_menu_button` had to be hacked together Signed-off-by: kernelkind --- src/ui/note/mod.rs | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/ui/note/mod.rs b/src/ui/note/mod.rs index 62509a7..cbe5155 100644 --- a/src/ui/note/mod.rs +++ b/src/ui/note/mod.rs @@ -19,7 +19,7 @@ use crate::{ notecache::{CachedNote, NoteCache}, ui::{self, View}, }; -use egui::{Align, Id, Label, Layout, Response, RichText, Sense}; +use egui::{menu::BarState, Align, Id, InnerResponse, Label, Layout, Response, RichText, Sense}; use enostr::NoteId; use nostrdb::{Ndb, Note, NoteKey, NoteReply, Transaction}; @@ -412,7 +412,7 @@ impl<'a> NoteView<'a> { if use_options_button { ui.with_layout(Layout::right_to_left(Align::Center), |ui| { let more_options_resp = more_options_button(ui, note_key, 8.0); - options_context_menu(more_options_resp) + options_context_menu(ui, more_options_resp) }) .inner } else { @@ -728,10 +728,13 @@ fn more_options_button(ui: &mut egui::Ui, note_key: NoteKey, max_height: f32) -> response } -fn options_context_menu(more_options_button_resp: egui::Response) -> Option { +fn options_context_menu( + ui: &mut egui::Ui, + more_options_button_resp: egui::Response, +) -> Option { let mut selected_option: Option = None; - more_options_button_resp.context_menu(|ui| { + stationary_arbitrary_menu_button(ui, more_options_button_resp, |ui| { ui.set_max_width(200.0); if ui.button("Copy text").clicked() { selected_option = Some(NoteOptionSelection::CopyText); @@ -746,5 +749,20 @@ fn options_context_menu(more_options_button_resp: egui::Response) -> Option( + ui: &mut egui::Ui, + button_response: egui::Response, + add_contents: impl FnOnce(&mut egui::Ui) -> R, +) -> InnerResponse> { + let bar_id = ui.id(); + let mut bar_state = BarState::load(ui.ctx(), bar_id); + + let inner = bar_state.bar_menu(&button_response, add_contents); + + bar_state.store(ui.ctx(), bar_id); + InnerResponse::new(inner.map(|r| r.inner), button_response) +} From 171889b3aa42bfdb836aafaa54f0ead7697fa307 Mon Sep 17 00:00:00 2001 From: kernelkind Date: Wed, 25 Sep 2024 13:13:59 -0400 Subject: [PATCH 3/9] process 'more options' for previews forgot to add this part Signed-off-by: kernelkind --- src/ui/note/contents.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/ui/note/contents.rs b/src/ui/note/contents.rs index e48aa48..327b9bd 100644 --- a/src/ui/note/contents.rs +++ b/src/ui/note/contents.rs @@ -1,5 +1,6 @@ use crate::images::ImageType; use crate::imgcache::ImageCache; +use crate::note_options::process_note_selection; use crate::notecache::NoteCache; use crate::ui::note::NoteOptions; use crate::ui::ProfilePic; @@ -103,13 +104,15 @@ pub fn render_note_preview( ui.visuals().noninteractive().bg_stroke.color, )) .show(ui, |ui| { - ui::NoteView::new(ndb, note_cache, img_cache, ¬e) + let resp = ui::NoteView::new(ndb, note_cache, img_cache, ¬e) .actionbar(false) .small_pfp(true) .wide(true) .note_previews(false) .use_more_options_button(true) .show(ui); + + process_note_selection(ui, resp.option_selection, ¬e); }) .response } From 1a94c21d96da14c533d2fdd894b4c8460337a575 Mon Sep 17 00:00:00 2001 From: William Casarin Date: Thu, 26 Sep 2024 09:22:05 -0700 Subject: [PATCH 4/9] refactor: remove processs_note_selection Moved this to NoteOptionSelection::process Signed-off-by: William Casarin --- src/note_options.rs | 10 +++------- src/ui/note/contents.rs | 5 +++-- src/ui/thread.rs | 7 ++++--- src/ui/timeline.rs | 5 +++-- 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/note_options.rs b/src/note_options.rs index 580253c..adbf4e1 100644 --- a/src/note_options.rs +++ b/src/note_options.rs @@ -9,13 +9,9 @@ pub enum NoteOptionSelection { CopyNoteId, } -pub fn process_note_selection( - ui: &mut egui::Ui, - selection: Option, - note: &Note<'_>, -) { - if let Some(option) = selection { - match option { +impl NoteOptionSelection { + pub fn process(&self, ui: &mut egui::Ui, note: &Note<'_>) { + match self { NoteOptionSelection::CopyText => { ui.output_mut(|w| { w.copied_text = note.content().to_string(); diff --git a/src/ui/note/contents.rs b/src/ui/note/contents.rs index 327b9bd..d0bdc1f 100644 --- a/src/ui/note/contents.rs +++ b/src/ui/note/contents.rs @@ -1,6 +1,5 @@ use crate::images::ImageType; use crate::imgcache::ImageCache; -use crate::note_options::process_note_selection; use crate::notecache::NoteCache; use crate::ui::note::NoteOptions; use crate::ui::ProfilePic; @@ -112,7 +111,9 @@ pub fn render_note_preview( .use_more_options_button(true) .show(ui); - process_note_selection(ui, resp.option_selection, ¬e); + if let Some(selection) = resp.option_selection { + selection.process(ui, ¬e); + } }) .response } diff --git a/src/ui/thread.rs b/src/ui/thread.rs index 3f67f04..6376e8a 100644 --- a/src/ui/thread.rs +++ b/src/ui/thread.rs @@ -1,6 +1,5 @@ use crate::{ - actionbar::BarAction, imgcache::ImageCache, note_options::process_note_selection, - notecache::NoteCache, thread::Threads, ui, + actionbar::BarAction, imgcache::ImageCache, notecache::NoteCache, thread::Threads, ui, }; use nostrdb::{Ndb, NoteKey, Transaction}; use tracing::{error, warn}; @@ -126,7 +125,9 @@ impl<'a> ThreadView<'a> { action = Some(bar_action); } - process_note_selection(ui, note_response.option_selection, ¬e); + if let Some(selection) = note_response.option_selection { + selection.process(ui, ¬e); + } }); ui::hline(ui); diff --git a/src/ui/timeline.rs b/src/ui/timeline.rs index a9e112e..8aad4a4 100644 --- a/src/ui/timeline.rs +++ b/src/ui/timeline.rs @@ -1,5 +1,4 @@ use crate::draft::Draft; -use crate::note_options::process_note_selection; use crate::{ actionbar::BarAction, column::Columns, imgcache::ImageCache, notecache::NoteCache, timeline::TimelineId, ui, @@ -159,7 +158,9 @@ fn timeline_ui( debug!("clicked note"); } - process_note_selection(ui, resp.option_selection, ¬e); + if let Some(selection) = resp.option_selection { + selection.process(ui, ¬e); + } }); ui::hline(ui); From a9cb734ef6add0f56ab4f8edc04521e8d0905fae Mon Sep 17 00:00:00 2001 From: William Casarin Date: Thu, 26 Sep 2024 09:31:11 -0700 Subject: [PATCH 5/9] refactor: make options_button a NoteOptions No reason why this needs to be a standalone bool Signed-off-by: William Casarin --- src/ui/note/contents.rs | 2 +- src/ui/note/mod.rs | 18 +++++++---------- src/ui/note/options.rs | 43 ++++++++++++++++++++--------------------- src/ui/note/reply.rs | 2 +- src/ui/thread.rs | 2 +- src/ui/timeline.rs | 2 +- 6 files changed, 32 insertions(+), 37 deletions(-) diff --git a/src/ui/note/contents.rs b/src/ui/note/contents.rs index d0bdc1f..cb55050 100644 --- a/src/ui/note/contents.rs +++ b/src/ui/note/contents.rs @@ -108,7 +108,7 @@ pub fn render_note_preview( .small_pfp(true) .wide(true) .note_previews(false) - .use_more_options_button(true) + .options_button(true) .show(ui); if let Some(selection) = resp.option_selection { diff --git a/src/ui/note/mod.rs b/src/ui/note/mod.rs index cbe5155..6af5b21 100644 --- a/src/ui/note/mod.rs +++ b/src/ui/note/mod.rs @@ -31,7 +31,6 @@ pub struct NoteView<'a> { img_cache: &'a mut ImageCache, note: &'a nostrdb::Note<'a>, flags: NoteOptions, - use_options: bool, } pub struct NoteResponse { @@ -201,7 +200,6 @@ impl<'a> NoteView<'a> { img_cache, note, flags, - use_options: false, } } @@ -240,11 +238,9 @@ impl<'a> NoteView<'a> { self } - pub fn use_more_options_button(self, enable: bool) -> Self { - Self { - use_options: enable, - ..self - } + pub fn options_button(mut self, enable: bool) -> Self { + self.options_mut().set_options_button(enable); + self } pub fn options(&self) -> NoteOptions { @@ -398,7 +394,7 @@ impl<'a> NoteView<'a> { note_cache: &mut NoteCache, note: &Note, profile: &Result, nostrdb::Error>, - use_options_button: bool, + options: NoteOptions, ) -> NoteResponse { let note_key = note.key().unwrap(); @@ -409,7 +405,7 @@ impl<'a> NoteView<'a> { let cached_note = note_cache.cached_note_or_insert_mut(note_key, note); render_reltime(ui, cached_note, true); - if use_options_button { + if options.has_options_button() { ui.with_layout(Layout::right_to_left(Align::Center), |ui| { let more_options_resp = more_options_button(ui, note_key, 8.0); options_context_menu(ui, more_options_resp) @@ -447,7 +443,7 @@ impl<'a> NoteView<'a> { self.note_cache, self.note, &profile, - self.use_options, + self.options(), ) .option_selection; }) @@ -494,7 +490,7 @@ impl<'a> NoteView<'a> { self.note_cache, self.note, &profile, - self.use_options, + self.options(), ) .option_selection; ui.horizontal(|ui| { diff --git a/src/ui/note/options.rs b/src/ui/note/options.rs index 371e8b4..12cf04f 100644 --- a/src/ui/note/options.rs +++ b/src/ui/note/options.rs @@ -5,14 +5,15 @@ bitflags! { // Attributes can be applied to flags types #[repr(transparent)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] - pub struct NoteOptions: u32 { - const actionbar = 0b00000001; - const note_previews = 0b00000010; - const small_pfp = 0b00000100; - const medium_pfp = 0b00001000; - const wide = 0b00010000; - const selectable_text = 0b00100000; - const textmode = 0b01000000; + pub struct NoteOptions: u64 { + const actionbar = 0b0000000000000001; + const note_previews = 0b0000000000000010; + const small_pfp = 0b0000000000000100; + const medium_pfp = 0b0000000000001000; + const wide = 0b0000000000010000; + const selectable_text = 0b0000000000100000; + const textmode = 0b0000000001000000; + const options_button = 0b0000000010000000; } } @@ -36,6 +37,8 @@ impl NoteOptions { create_setter!(set_selectable_text, selectable_text); create_setter!(set_textmode, textmode); create_setter!(set_actionbar, actionbar); + create_setter!(set_wide, wide); + create_setter!(set_options_button, options_button); #[inline] pub fn has_actionbar(self) -> bool { @@ -67,27 +70,23 @@ impl NoteOptions { (self & NoteOptions::medium_pfp) == NoteOptions::medium_pfp } - pub fn pfp_size(&self) -> f32 { - if self.has_small_pfp() { - ProfilePic::small_size() - } else if self.has_medium_pfp() { - ProfilePic::medium_size() - } else { - ProfilePic::default_size() - } - } - #[inline] pub fn has_wide(self) -> bool { (self & NoteOptions::wide) == NoteOptions::wide } #[inline] - pub fn set_wide(&mut self, enable: bool) { - if enable { - *self |= NoteOptions::wide; + pub fn has_options_button(self) -> bool { + (self & NoteOptions::options_button) == NoteOptions::options_button + } + + pub fn pfp_size(&self) -> f32 { + if self.has_small_pfp() { + ProfilePic::small_size() + } else if self.has_medium_pfp() { + ProfilePic::medium_size() } else { - *self &= !NoteOptions::wide; + ProfilePic::default_size() } } } diff --git a/src/ui/note/reply.rs b/src/ui/note/reply.rs index c2ed606..c34d140 100644 --- a/src/ui/note/reply.rs +++ b/src/ui/note/reply.rs @@ -67,7 +67,7 @@ impl<'a> PostReplyView<'a> { ui::NoteView::new(self.ndb, self.note_cache, self.img_cache, self.note) .actionbar(false) .medium_pfp(true) - .use_more_options_button(true) + .options_button(true) .show(ui); }); diff --git a/src/ui/thread.rs b/src/ui/thread.rs index 6376e8a..76ca581 100644 --- a/src/ui/thread.rs +++ b/src/ui/thread.rs @@ -119,7 +119,7 @@ impl<'a> ThreadView<'a> { ui::NoteView::new(self.ndb, self.note_cache, self.img_cache, ¬e) .note_previews(!self.textmode) .textmode(self.textmode) - .use_more_options_button(!self.textmode) + .options_button(!self.textmode) .show(ui); if let Some(bar_action) = note_response.action { action = Some(bar_action); diff --git a/src/ui/timeline.rs b/src/ui/timeline.rs index 8aad4a4..bbea0a5 100644 --- a/src/ui/timeline.rs +++ b/src/ui/timeline.rs @@ -149,7 +149,7 @@ fn timeline_ui( let resp = ui::NoteView::new(ndb, note_cache, img_cache, ¬e) .note_previews(!textmode) .selectable_text(false) - .use_more_options_button(true) + .options_button(true) .show(ui); if let Some(ba) = resp.action { From 0c3b2ae81715c88352edce5e9757c0c075d0520b Mon Sep 17 00:00:00 2001 From: William Casarin Date: Thu, 26 Sep 2024 09:40:42 -0700 Subject: [PATCH 6/9] note: switch to muted menu_options_button color Otherwise it stands out too much Signed-off-by: William Casarin --- src/ui/note/mod.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/ui/note/mod.rs b/src/ui/note/mod.rs index 6af5b21..87690f1 100644 --- a/src/ui/note/mod.rs +++ b/src/ui/note/mod.rs @@ -709,11 +709,8 @@ fn more_options_button(ui: &mut egui::Ui, note_key: NoteKey, max_height: f32) -> let translated_radius = (cur_radius - 1.0) / 2.0; - let color = if ui.style().visuals.dark_mode { - egui::Color32::WHITE - } else { - egui::Color32::BLACK - }; + // This works in both themes + let color = colors::GRAY_SECONDARY; // Draw circles let painter = ui.painter_at(rect); From 2dba41186d6298724dbd3b3da757a160f4a2dd32 Mon Sep 17 00:00:00 2001 From: William Casarin Date: Thu, 26 Sep 2024 10:47:43 -0700 Subject: [PATCH 7/9] context: move note context button to its own file Signed-off-by: William Casarin --- src/lib.rs | 1 - src/note_options.rs | 36 --------- src/ui/note/contents.rs | 4 +- src/ui/note/context.rs | 158 ++++++++++++++++++++++++++++++++++++++++ src/ui/note/mod.rs | 103 ++++---------------------- src/ui/thread.rs | 2 +- src/ui/timeline.rs | 4 +- 7 files changed, 176 insertions(+), 132 deletions(-) delete mode 100644 src/note_options.rs create mode 100644 src/ui/note/context.rs diff --git a/src/lib.rs b/src/lib.rs index 676d87e..22610e4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,7 +23,6 @@ pub mod login_manager; mod macos_key_storage; mod nav; mod note; -mod note_options; mod notecache; mod post; mod post_action_executor; diff --git a/src/note_options.rs b/src/note_options.rs deleted file mode 100644 index adbf4e1..0000000 --- a/src/note_options.rs +++ /dev/null @@ -1,36 +0,0 @@ -use enostr::{NoteId, Pubkey}; -use nostrdb::Note; - -#[derive(Clone)] -#[allow(clippy::enum_variant_names)] -pub enum NoteOptionSelection { - CopyText, - CopyPubkey, - CopyNoteId, -} - -impl NoteOptionSelection { - pub fn process(&self, ui: &mut egui::Ui, note: &Note<'_>) { - match self { - NoteOptionSelection::CopyText => { - ui.output_mut(|w| { - w.copied_text = note.content().to_string(); - }); - } - NoteOptionSelection::CopyPubkey => { - ui.output_mut(|w| { - if let Some(bech) = Pubkey::new(*note.pubkey()).to_bech() { - w.copied_text = bech; - } - }); - } - NoteOptionSelection::CopyNoteId => { - ui.output_mut(|w| { - if let Some(bech) = NoteId::new(*note.id()).to_bech() { - w.copied_text = bech; - } - }); - } - } - } -} diff --git a/src/ui/note/contents.rs b/src/ui/note/contents.rs index cb55050..084eb75 100644 --- a/src/ui/note/contents.rs +++ b/src/ui/note/contents.rs @@ -111,8 +111,8 @@ pub fn render_note_preview( .options_button(true) .show(ui); - if let Some(selection) = resp.option_selection { - selection.process(ui, ¬e); + if let Some(context) = resp.context_selection { + context.process(ui, ¬e); } }) .response diff --git a/src/ui/note/context.rs b/src/ui/note/context.rs new file mode 100644 index 0000000..ba4aed4 --- /dev/null +++ b/src/ui/note/context.rs @@ -0,0 +1,158 @@ +use crate::colors; +use egui::Vec2; +use enostr::{NoteId, Pubkey}; +use nostrdb::{Note, NoteKey}; + +#[derive(Clone)] +#[allow(clippy::enum_variant_names)] +pub enum NoteContextSelection { + CopyText, + CopyPubkey, + CopyNoteId, +} + +impl NoteContextSelection { + pub fn process(&self, ui: &mut egui::Ui, note: &Note<'_>) { + match self { + NoteContextSelection::CopyText => { + ui.output_mut(|w| { + w.copied_text = note.content().to_string(); + }); + } + NoteContextSelection::CopyPubkey => { + ui.output_mut(|w| { + if let Some(bech) = Pubkey::new(*note.pubkey()).to_bech() { + w.copied_text = bech; + } + }); + } + NoteContextSelection::CopyNoteId => { + ui.output_mut(|w| { + if let Some(bech) = NoteId::new(*note.id()).to_bech() { + w.copied_text = bech; + } + }); + } + } + } +} + +pub struct NoteContextButton { + note_key: NoteKey, +} + +impl egui::Widget for NoteContextButton { + fn ui(self, ui: &mut egui::Ui) -> egui::Response { + Self::show(ui, self.note_key) + } +} + +impl NoteContextButton { + pub fn new(note_key: NoteKey) -> Self { + NoteContextButton { note_key } + } + + pub fn max_width() -> f32 { + Self::max_radius() * 3.0 + Self::max_distance_between_circles() * 2.0 + } + + pub fn size() -> Vec2 { + let width = Self::max_width(); + egui::vec2(width, width) + } + + fn max_radius() -> f32 { + 8.0 + } + + fn min_radius() -> f32 { + Self::max_radius() / Self::expansion_multiple() + } + + fn max_distance_between_circles() -> f32 { + 2.0 + } + + fn expansion_multiple() -> f32 { + 2.0 + } + + fn min_distance_between_circles() -> f32 { + Self::max_distance_between_circles() / Self::expansion_multiple() + } + + pub fn show(ui: &mut egui::Ui, note_key: NoteKey) -> egui::Response { + let id = ui.id().with(("more_options_anim", note_key)); + + let min_radius = Self::min_radius(); + let anim_speed = 0.05; + let size = Self::size(); + let (rect, response) = ui.allocate_exact_size(size, egui::Sense::click()); + + let animation_progress = + ui.ctx() + .animate_bool_with_time(id, response.hovered(), anim_speed); + + let min_distance = Self::min_distance_between_circles(); + let cur_distance = min_distance + + (Self::max_distance_between_circles() - min_distance) * animation_progress; + + let cur_radius = min_radius + (Self::max_radius() - min_radius) * animation_progress; + + let center = rect.center(); + let left_circle_center = center - egui::vec2(cur_distance + cur_radius, 0.0); + let right_circle_center = center + egui::vec2(cur_distance + cur_radius, 0.0); + + let translated_radius = (cur_radius - 1.0) / 2.0; + + // This works in both themes + let color = colors::GRAY_SECONDARY; + + // Draw circles + let painter = ui.painter_at(rect); + painter.circle_filled(left_circle_center, translated_radius, color); + painter.circle_filled(center, translated_radius, color); + painter.circle_filled(right_circle_center, translated_radius, color); + + response + } + + pub fn menu( + ui: &mut egui::Ui, + button_response: egui::Response, + ) -> Option { + let mut context_selection: Option = None; + + stationary_arbitrary_menu_button(ui, button_response, |ui| { + ui.set_max_width(200.0); + if ui.button("Copy text").clicked() { + context_selection = Some(NoteContextSelection::CopyText); + ui.close_menu(); + } + if ui.button("Copy user public key").clicked() { + context_selection = Some(NoteContextSelection::CopyPubkey); + ui.close_menu(); + } + if ui.button("Copy note id").clicked() { + context_selection = Some(NoteContextSelection::CopyNoteId); + ui.close_menu(); + } + }); + + context_selection + } +} + +fn stationary_arbitrary_menu_button( + ui: &mut egui::Ui, + button_response: egui::Response, + add_contents: impl FnOnce(&mut egui::Ui) -> R, +) -> egui::InnerResponse> { + let bar_id = ui.id(); + let mut bar_state = egui::menu::BarState::load(ui.ctx(), bar_id); + + let inner = bar_state.bar_menu(&button_response, add_contents); + + bar_state.store(ui.ctx(), bar_id); + egui::InnerResponse::new(inner.map(|r| r.inner), button_response) +} diff --git a/src/ui/note/mod.rs b/src/ui/note/mod.rs index 87690f1..6d9a5fb 100644 --- a/src/ui/note/mod.rs +++ b/src/ui/note/mod.rs @@ -1,10 +1,12 @@ pub mod contents; +pub mod context; pub mod options; pub mod post; pub mod quote_repost; pub mod reply; pub use contents::NoteContents; +pub use context::{NoteContextButton, NoteContextSelection}; pub use options::NoteOptions; pub use post::{PostAction, PostResponse, PostView}; pub use quote_repost::QuoteRepostView; @@ -15,11 +17,10 @@ use crate::{ app_style::NotedeckTextStyle, colors, imgcache::ImageCache, - note_options::NoteOptionSelection, notecache::{CachedNote, NoteCache}, ui::{self, View}, }; -use egui::{menu::BarState, Align, Id, InnerResponse, Label, Layout, Response, RichText, Sense}; +use egui::{Id, Label, Response, RichText, Sense}; use enostr::NoteId; use nostrdb::{Ndb, Note, NoteKey, NoteReply, Transaction}; @@ -36,7 +37,7 @@ pub struct NoteView<'a> { pub struct NoteResponse { pub response: egui::Response, pub action: Option, - pub option_selection: Option, + pub context_selection: Option, } impl NoteResponse { @@ -44,7 +45,7 @@ impl NoteResponse { Self { response, action: None, - option_selection: None, + context_selection: None, } } @@ -52,9 +53,9 @@ impl NoteResponse { Self { action, ..self } } - pub fn select_option(self, option_selection: Option) -> Self { + pub fn select_option(self, context_selection: Option) -> Self { Self { - option_selection, + context_selection, ..self } } @@ -406,9 +407,9 @@ impl<'a> NoteView<'a> { render_reltime(ui, cached_note, true); if options.has_options_button() { - ui.with_layout(Layout::right_to_left(Align::Center), |ui| { - let more_options_resp = more_options_button(ui, note_key, 8.0); - options_context_menu(ui, more_options_resp) + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + let resp = ui.add(NoteContextButton::new(note_key)); + NoteContextButton::menu(ui, resp) }) .inner } else { @@ -425,7 +426,7 @@ impl<'a> NoteView<'a> { let note_key = self.note.key().expect("todo: support non-db notes"); let txn = self.note.txn().expect("todo: support non-db notes"); let mut note_action: Option = None; - let mut selected_option: Option = None; + let mut selected_option: Option = None; let profile = self.ndb.get_profile_by_pubkey(txn, self.note.pubkey()); let maybe_hitbox = maybe_note_hitbox(ui, note_key); @@ -445,7 +446,7 @@ impl<'a> NoteView<'a> { &profile, self.options(), ) - .option_selection; + .context_selection; }) .response }); @@ -492,7 +493,7 @@ impl<'a> NoteView<'a> { &profile, self.options(), ) - .option_selection; + .context_selection; ui.horizontal(|ui| { ui.spacing_mut().item_spacing.x = 2.0; @@ -681,81 +682,3 @@ fn quote_repost_button(ui: &mut egui::Ui, note_key: NoteKey) -> egui::Response { resp.union(put_resp) } - -fn more_options_button(ui: &mut egui::Ui, note_key: NoteKey, max_height: f32) -> egui::Response { - let id = ui.id().with(("more_options_anim", note_key)); - - let expansion_multiple = 2.0; - let max_radius = max_height; - let min_radius = max_radius / expansion_multiple; - let max_distance_between_circles = 2.0; - let min_distance_between_circles = max_distance_between_circles / expansion_multiple; - let max_width = max_radius * 3.0 + max_distance_between_circles * 2.0; - - let anim_speed = 0.05; - let expanded_size = egui::vec2(max_width, max_height); - let (rect, response) = ui.allocate_exact_size(expanded_size, egui::Sense::click()); - - let animation_progress = ui - .ctx() - .animate_bool_with_time(id, response.hovered(), anim_speed); - let cur_distance = min_distance_between_circles - + (max_distance_between_circles - min_distance_between_circles) * animation_progress; - let cur_radius = min_radius + (max_radius - min_radius) * animation_progress; - - let center = rect.center(); - let left_circle_center = center - egui::vec2(cur_distance + cur_radius, 0.0); - let right_circle_center = center + egui::vec2(cur_distance + cur_radius, 0.0); - - let translated_radius = (cur_radius - 1.0) / 2.0; - - // This works in both themes - let color = colors::GRAY_SECONDARY; - - // Draw circles - let painter = ui.painter_at(rect); - painter.circle_filled(left_circle_center, translated_radius, color); - painter.circle_filled(center, translated_radius, color); - painter.circle_filled(right_circle_center, translated_radius, color); - - response -} - -fn options_context_menu( - ui: &mut egui::Ui, - more_options_button_resp: egui::Response, -) -> Option { - let mut selected_option: Option = None; - - stationary_arbitrary_menu_button(ui, more_options_button_resp, |ui| { - ui.set_max_width(200.0); - if ui.button("Copy text").clicked() { - selected_option = Some(NoteOptionSelection::CopyText); - ui.close_menu(); - } - if ui.button("Copy user public key").clicked() { - selected_option = Some(NoteOptionSelection::CopyPubkey); - ui.close_menu(); - } - if ui.button("Copy note id").clicked() { - selected_option = Some(NoteOptionSelection::CopyNoteId); - ui.close_menu(); - } - }); - - selected_option -} - -fn stationary_arbitrary_menu_button( - ui: &mut egui::Ui, - button_response: egui::Response, - add_contents: impl FnOnce(&mut egui::Ui) -> R, -) -> InnerResponse> { - let bar_id = ui.id(); - let mut bar_state = BarState::load(ui.ctx(), bar_id); - - let inner = bar_state.bar_menu(&button_response, add_contents); - - bar_state.store(ui.ctx(), bar_id); - InnerResponse::new(inner.map(|r| r.inner), button_response) -} diff --git a/src/ui/thread.rs b/src/ui/thread.rs index 76ca581..44ff6f8 100644 --- a/src/ui/thread.rs +++ b/src/ui/thread.rs @@ -125,7 +125,7 @@ impl<'a> ThreadView<'a> { action = Some(bar_action); } - if let Some(selection) = note_response.option_selection { + if let Some(selection) = note_response.context_selection { selection.process(ui, ¬e); } }); diff --git a/src/ui/timeline.rs b/src/ui/timeline.rs index bbea0a5..f0e4ace 100644 --- a/src/ui/timeline.rs +++ b/src/ui/timeline.rs @@ -158,8 +158,8 @@ fn timeline_ui( debug!("clicked note"); } - if let Some(selection) = resp.option_selection { - selection.process(ui, ¬e); + if let Some(context) = resp.context_selection { + context.process(ui, ¬e); } }); From 5120686679f0c0d2d595b33e5453f1ec69d5bbb6 Mon Sep 17 00:00:00 2001 From: William Casarin Date: Thu, 26 Sep 2024 12:17:12 -0700 Subject: [PATCH 8/9] context: fix hitbox, float on far right This updates the context menu to "float" instead of using the layout engine. This is so that we don't take up an unnecessary amount of space when we increase the hitbox height. Signed-off-by: William Casarin --- src/ui/note/context.rs | 32 ++++++++++++++++++++++++-------- src/ui/note/mod.rs | 24 ++++++++++++++++++------ 2 files changed, 42 insertions(+), 14 deletions(-) diff --git a/src/ui/note/context.rs b/src/ui/note/context.rs index ba4aed4..5ce9bf6 100644 --- a/src/ui/note/context.rs +++ b/src/ui/note/context.rs @@ -1,5 +1,5 @@ use crate::colors; -use egui::Vec2; +use egui::{Rect, Vec2}; use enostr::{NoteId, Pubkey}; use nostrdb::{Note, NoteKey}; @@ -38,18 +38,35 @@ impl NoteContextSelection { } pub struct NoteContextButton { + put_at: Option, note_key: NoteKey, } impl egui::Widget for NoteContextButton { fn ui(self, ui: &mut egui::Ui) -> egui::Response { - Self::show(ui, self.note_key) + let r = if let Some(r) = self.put_at { + r + } else { + let mut place = ui.available_rect_before_wrap(); + let size = Self::max_width(); + place.set_width(size); + place.set_height(size); + place + }; + + Self::show(ui, self.note_key, r) } } impl NoteContextButton { pub fn new(note_key: NoteKey) -> Self { - NoteContextButton { note_key } + let put_at: Option = None; + NoteContextButton { note_key, put_at } + } + + pub fn place_at(mut self, rect: Rect) -> Self { + self.put_at = Some(rect); + self } pub fn max_width() -> f32 { @@ -81,13 +98,12 @@ impl NoteContextButton { Self::max_distance_between_circles() / Self::expansion_multiple() } - pub fn show(ui: &mut egui::Ui, note_key: NoteKey) -> egui::Response { + pub fn show(ui: &mut egui::Ui, note_key: NoteKey, put_at: Rect) -> egui::Response { let id = ui.id().with(("more_options_anim", note_key)); let min_radius = Self::min_radius(); let anim_speed = 0.05; - let size = Self::size(); - let (rect, response) = ui.allocate_exact_size(size, egui::Sense::click()); + let response = ui.interact(put_at, id, egui::Sense::click()); let animation_progress = ui.ctx() @@ -99,7 +115,7 @@ impl NoteContextButton { let cur_radius = min_radius + (Self::max_radius() - min_radius) * animation_progress; - let center = rect.center(); + let center = put_at.center(); let left_circle_center = center - egui::vec2(cur_distance + cur_radius, 0.0); let right_circle_center = center + egui::vec2(cur_distance + cur_radius, 0.0); @@ -109,7 +125,7 @@ impl NoteContextButton { let color = colors::GRAY_SECONDARY; // Draw circles - let painter = ui.painter_at(rect); + let painter = ui.painter_at(put_at); painter.circle_filled(left_circle_center, translated_radius, color); painter.circle_filled(center, translated_radius, color); painter.circle_filled(right_circle_center, translated_radius, color); diff --git a/src/ui/note/mod.rs b/src/ui/note/mod.rs index 6d9a5fb..544d242 100644 --- a/src/ui/note/mod.rs +++ b/src/ui/note/mod.rs @@ -20,7 +20,7 @@ use crate::{ notecache::{CachedNote, NoteCache}, ui::{self, View}, }; -use egui::{Id, Label, Response, RichText, Sense}; +use egui::{Id, Label, Pos2, Rect, Response, RichText, Sense}; use enostr::NoteId; use nostrdb::{Ndb, Note, NoteKey, NoteReply, Transaction}; @@ -396,6 +396,7 @@ impl<'a> NoteView<'a> { note: &Note, profile: &Result, nostrdb::Error>, options: NoteOptions, + container_right: Pos2, ) -> NoteResponse { let note_key = note.key().unwrap(); @@ -407,11 +408,14 @@ impl<'a> NoteView<'a> { render_reltime(ui, cached_note, true); if options.has_options_button() { - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - let resp = ui.add(NoteContextButton::new(note_key)); - NoteContextButton::menu(ui, resp) - }) - .inner + let context_pos = { + let size = NoteContextButton::max_width(); + let min = Pos2::new(container_right.x - size, container_right.y); + Rect::from_min_size(min, egui::vec2(size, size)) + }; + + let resp = ui.add(NoteContextButton::new(note_key).place_at(context_pos)); + NoteContextButton::menu(ui, resp.clone()) } else { None } @@ -429,6 +433,12 @@ impl<'a> NoteView<'a> { let mut selected_option: Option = None; let profile = self.ndb.get_profile_by_pubkey(txn, self.note.pubkey()); let maybe_hitbox = maybe_note_hitbox(ui, note_key); + let container_right = { + let r = ui.available_rect_before_wrap(); + let x = r.max.x; + let y = r.min.y; + Pos2::new(x, y) + }; // wide design let response = if self.options().has_wide() { @@ -445,6 +455,7 @@ impl<'a> NoteView<'a> { self.note, &profile, self.options(), + container_right, ) .context_selection; }) @@ -492,6 +503,7 @@ impl<'a> NoteView<'a> { self.note, &profile, self.options(), + container_right, ) .context_selection; ui.horizontal(|ui| { From d416044f47ca892f4f58e56b753bed430c68f121 Mon Sep 17 00:00:00 2001 From: William Casarin Date: Thu, 26 Sep 2024 12:29:29 -0700 Subject: [PATCH 9/9] context: set cursor icon on hover So we know its clickable. I mean the animation signals that as well, but still. Signed-off-by: William Casarin --- src/ui/note/context.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/ui/note/context.rs b/src/ui/note/context.rs index 5ce9bf6..342af4b 100644 --- a/src/ui/note/context.rs +++ b/src/ui/note/context.rs @@ -105,9 +105,12 @@ impl NoteContextButton { let anim_speed = 0.05; let response = ui.interact(put_at, id, egui::Sense::click()); - let animation_progress = - ui.ctx() - .animate_bool_with_time(id, response.hovered(), anim_speed); + let hovered = response.hovered(); + let animation_progress = ui.ctx().animate_bool_with_time(id, hovered, anim_speed); + + if hovered { + ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand); + } let min_distance = Self::min_distance_between_circles(); let cur_distance = min_distance