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 a46c75f1..ad8c8494 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 22610e4c..676d87ea 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 00000000..580253c5 --- /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 82c17984..51bdc757 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 21eb1154..e48aa48e 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 491905b8..62509a7e 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 a46ac5e6..c2ed606d 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 0abf67f4..3f67f04f 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 bc1e02bd..a9e112e1 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 62509a7e..cbe51557 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 e48aa48e..327b9bd5 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 580253c5..adbf4e17 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 327b9bd5..d0bdc1fc 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 3f67f04f..6376e8a8 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 a9e112e1..8aad4a48 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 d0bdc1fc..cb55050e 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 cbe51557..6af5b21d 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 371e8b48..12cf04fe 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,6 +70,16 @@ impl NoteOptions { (self & NoteOptions::medium_pfp) == NoteOptions::medium_pfp } + #[inline] + pub fn has_wide(self) -> bool { + (self & NoteOptions::wide) == NoteOptions::wide + } + + #[inline] + 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() @@ -76,18 +89,4 @@ impl NoteOptions { 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; - } else { - *self &= !NoteOptions::wide; - } - } } diff --git a/src/ui/note/reply.rs b/src/ui/note/reply.rs index c2ed606d..c34d1403 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 6376e8a8..76ca581f 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 8aad4a48..bbea0a5c 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 6af5b21d..87690f14 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 676d87ea..22610e4c 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 adbf4e17..00000000 --- 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 cb55050e..084eb751 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 00000000..ba4aed48 --- /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 87690f14..6d9a5fbb 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 76ca581f..44ff6f88 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 bbea0a5c..f0e4aced 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 ba4aed48..5ce9bf60 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 6d9a5fbb..544d2426 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 5ce9bf60..342af4b6 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