diff --git a/Cargo.lock b/Cargo.lock index 7d87574f..fa570f5f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1392,17 +1392,17 @@ checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" [[package]] name = "ecolor" version = "0.31.1" -source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc" +source = "git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd#a67ab901e197ce13948ff7d00aa6e07e31a68ccd" dependencies = [ "bytemuck", - "emath 0.31.1 (git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc)", + "emath 0.31.1 (git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd)", "serde", ] [[package]] name = "eframe" version = "0.31.1" -source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc" +source = "git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd#a67ab901e197ce13948ff7d00aa6e07e31a68ccd" dependencies = [ "ahash", "bytemuck", @@ -1438,24 +1438,25 @@ dependencies = [ [[package]] name = "egui" version = "0.31.1" -source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc" +source = "git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd#a67ab901e197ce13948ff7d00aa6e07e31a68ccd" dependencies = [ "accesskit", "ahash", "backtrace", "bitflags 2.9.1", - "emath 0.31.1 (git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc)", + "emath 0.31.1 (git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd)", "epaint", "log", "nohash-hasher", "profiling", "serde", + "similar", ] [[package]] name = "egui-wgpu" version = "0.31.1" -source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc" +source = "git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd#a67ab901e197ce13948ff7d00aa6e07e31a68ccd" dependencies = [ "ahash", "bytemuck", @@ -1474,7 +1475,7 @@ dependencies = [ [[package]] name = "egui-winit" version = "0.31.1" -source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc" +source = "git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd#a67ab901e197ce13948ff7d00aa6e07e31a68ccd" dependencies = [ "ahash", "arboard", @@ -1492,7 +1493,7 @@ dependencies = [ [[package]] name = "egui_extras" version = "0.31.1" -source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc" +source = "git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd#a67ab901e197ce13948ff7d00aa6e07e31a68ccd" dependencies = [ "ahash", "egui", @@ -1509,7 +1510,7 @@ dependencies = [ [[package]] name = "egui_glow" version = "0.31.1" -source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc" +source = "git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd#a67ab901e197ce13948ff7d00aa6e07e31a68ccd" dependencies = [ "ahash", "bytemuck", @@ -1588,7 +1589,7 @@ checksum = "9e4cadcff7a5353ba72b7fea76bf2122b5ebdbc68e8155aa56dfdea90083fe1b" [[package]] name = "emath" version = "0.31.1" -source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc" +source = "git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd#a67ab901e197ce13948ff7d00aa6e07e31a68ccd" dependencies = [ "bytemuck", "serde", @@ -1686,13 +1687,13 @@ dependencies = [ [[package]] name = "epaint" version = "0.31.1" -source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc" +source = "git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd#a67ab901e197ce13948ff7d00aa6e07e31a68ccd" dependencies = [ "ab_glyph", "ahash", "bytemuck", "ecolor", - "emath 0.31.1 (git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc)", + "emath 0.31.1 (git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd)", "epaint_default_fonts", "log", "nohash-hasher", @@ -1704,7 +1705,7 @@ dependencies = [ [[package]] name = "epaint_default_fonts" version = "0.31.1" -source = "git+https://github.com/damus-io/egui?rev=041d4d18b16cf8be97e0d7ef5892c87436352dfc#041d4d18b16cf8be97e0d7ef5892c87436352dfc" +source = "git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd#a67ab901e197ce13948ff7d00aa6e07e31a68ccd" [[package]] name = "equator" @@ -5348,6 +5349,12 @@ dependencies = [ "quote", ] +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" + [[package]] name = "simplecss" version = "0.2.2" diff --git a/Cargo.toml b/Cargo.toml index c9097ebc..289e064a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -101,12 +101,12 @@ strip = true # Strip symbols from binary* #egui_extras = { path = "/home/jb55/dev/github/emilk/egui/crates/egui_extras" } #epaint = { path = "/home/jb55/dev/github/emilk/egui/crates/epaint" } -egui = { git = "https://github.com/damus-io/egui", rev = "041d4d18b16cf8be97e0d7ef5892c87436352dfc" } -eframe = { git = "https://github.com/damus-io/egui", rev = "041d4d18b16cf8be97e0d7ef5892c87436352dfc" } -egui-winit = { git = "https://github.com/damus-io/egui", rev = "041d4d18b16cf8be97e0d7ef5892c87436352dfc" } -egui-wgpu = { git = "https://github.com/damus-io/egui", rev = "041d4d18b16cf8be97e0d7ef5892c87436352dfc" } -egui_extras = { git = "https://github.com/damus-io/egui", rev = "041d4d18b16cf8be97e0d7ef5892c87436352dfc" } -epaint = { git = "https://github.com/damus-io/egui", rev = "041d4d18b16cf8be97e0d7ef5892c87436352dfc" } +egui = { git = "https://github.com/damus-io/egui", rev = "a67ab901e197ce13948ff7d00aa6e07e31a68ccd" } +eframe = { git = "https://github.com/damus-io/egui", rev = "a67ab901e197ce13948ff7d00aa6e07e31a68ccd" } +egui-winit = { git = "https://github.com/damus-io/egui", rev = "a67ab901e197ce13948ff7d00aa6e07e31a68ccd" } +egui-wgpu = { git = "https://github.com/damus-io/egui", rev = "a67ab901e197ce13948ff7d00aa6e07e31a68ccd" } +egui_extras = { git = "https://github.com/damus-io/egui", rev = "a67ab901e197ce13948ff7d00aa6e07e31a68ccd" } +epaint = { git = "https://github.com/damus-io/egui", rev = "a67ab901e197ce13948ff7d00aa6e07e31a68ccd" } puffin = { git = "https://github.com/jb55/puffin", package = "puffin", rev = "c6a6242adaf90b6292c0f462d2acd34d96d224d2" } puffin_egui = { git = "https://github.com/jb55/puffin", package = "puffin_egui", rev = "c6a6242adaf90b6292c0f462d2acd34d96d224d2" } #winit = { git = "https://github.com/damus-io/winit", rev = "14d61a74bee0c9863abe7ef28efae2c4d8bd3743" } diff --git a/crates/notedeck_columns/src/post.rs b/crates/notedeck_columns/src/post.rs index 9cfaaf51..af3faf6b 100644 --- a/crates/notedeck_columns/src/post.rs +++ b/crates/notedeck_columns/src/post.rs @@ -1,4 +1,8 @@ -use egui::{text::LayoutJob, TextBuffer, TextFormat}; +use egui::{ + text::{CCursor, CCursorRange, LayoutJob}, + text_edit::TextEditOutput, + TextBuffer, TextEdit, TextFormat, +}; use enostr::{FullKeypair, Pubkey}; use nostrdb::{Note, NoteBuilder, NoteReply}; use std::{ @@ -270,6 +274,36 @@ impl Default for PostBuffer { } } +/// New cursor index (indexed by characters) after operation is performed +#[must_use = "must call MentionSelectedResponse::process"] +pub struct MentionSelectedResponse { + pub next_cursor_index: usize, +} + +impl MentionSelectedResponse { + pub fn process(&self, ctx: &egui::Context, text_edit_output: &TextEditOutput) { + let text_edit_id = text_edit_output.response.id; + let Some(mut before_state) = TextEdit::load_state(ctx, text_edit_id) else { + return; + }; + + let mut new_cursor = text_edit_output + .galley + .from_ccursor(CCursor::new(self.next_cursor_index)); + new_cursor.ccursor.prefer_next_row = true; + + before_state + .cursor + .set_char_range(Some(CCursorRange::one(CCursor::new( + self.next_cursor_index, + )))); + + ctx.memory_mut(|mem| mem.request_focus(text_edit_id)); + + TextEdit::store_state(ctx, text_edit_id, before_state); + } +} + impl PostBuffer { pub fn get_new_mentions_key(&mut self) -> usize { let prev = self.mentions_key; @@ -319,15 +353,21 @@ impl PostBuffer { mention_key: usize, full_name: &str, pk: Pubkey, - ) { - if let Some(info) = self.mentions.get(&mention_key) { - let text_start_index = info.start_index + 1; - self.delete_char_range(text_start_index..info.end_index); - self.insert_text(full_name, text_start_index); - self.select_full_mention(mention_key, pk); - } else { + ) -> Option { + let Some(info) = self.mentions.get(&mention_key) else { error!("Error selecting mention for index: {mention_key}. Have the following mentions: {:?}", self.mentions); - } + return None; + }; + let text_start_index = info.start_index + 1; // increment by one to exclude the mention indicator, '@' + self.delete_char_range(text_start_index..info.end_index); + let text_chars_inserted = self.insert_text(full_name, text_start_index); + self.select_full_mention(mention_key, pk); + + let space_chars_inserted = self.insert_text(" ", text_start_index + text_chars_inserted); + + Some(MentionSelectedResponse { + next_cursor_index: text_start_index + text_chars_inserted + space_chars_inserted, + }) } pub fn delete_mention(&mut self, mention_key: usize) { @@ -917,9 +957,9 @@ mod tests { assert_eq!(buf.mentions.len(), 1); assert_eq!(buf.mentions.get(&0).unwrap().bounds(), 0..3); buf.select_mention_and_replace_name(0, "jb55", JB55()); - assert_eq!(buf.as_str(), "@jb55"); + assert_eq!(buf.as_str(), "@jb55 "); - buf.insert_text(" test", 5); + buf.insert_text("test", 6); assert_eq!(buf.as_str(), "@jb55 test"); assert_eq!(buf.mentions.len(), 1); @@ -1201,16 +1241,20 @@ mod tests { buf.insert_text("@jb", 0); buf.select_mention_and_replace_name(0, "jb55", JB55()); - buf.insert_text(" test ", 5); + buf.insert_text("test ", 6); + assert_eq!(buf.as_str(), "@jb55 test "); buf.insert_text("@kernel", 11); buf.select_mention_and_replace_name(1, "KernelKind", KK()); - buf.insert_text(" test", 22); + assert_eq!(buf.as_str(), "@jb55 test @KernelKind "); + buf.insert_text("test", 23); assert_eq!(buf.as_str(), "@jb55 test @KernelKind test"); + assert_eq!(buf.mentions.len(), 2); - buf.insert_text(" ", 5); buf.insert_text("@els", 6); + assert_eq!(buf.as_str(), "@jb55 @elstest @KernelKind test"); + assert_eq!(buf.mentions.len(), 3); assert_eq!(buf.mentions.get(&2).unwrap().bounds(), 6..10); buf.select_mention_and_replace_name(2, "elsat", JB55()); diff --git a/crates/notedeck_columns/src/ui/search_results.rs b/crates/notedeck_columns/src/ui/mentions_picker.rs similarity index 79% rename from crates/notedeck_columns/src/ui/search_results.rs rename to crates/notedeck_columns/src/ui/mentions_picker.rs index 7f030443..43ad4aa7 100644 --- a/crates/notedeck_columns/src/ui/search_results.rs +++ b/crates/notedeck_columns/src/ui/mentions_picker.rs @@ -11,19 +11,21 @@ use notedeck_ui::{ }; use tracing::error; -pub struct SearchResultsView<'a> { +/// Displays user profiles for the user to pick from. +/// Useful for manually typing a username and selecting the profile desired +pub struct MentionPickerView<'a> { ndb: &'a Ndb, txn: &'a Transaction, img_cache: &'a mut Images, results: &'a Vec<&'a [u8; 32]>, } -pub enum SearchResultsResponse { +pub enum MentionPickerResponse { SelectResult(Option), DeleteMention, } -impl<'a> SearchResultsView<'a> { +impl<'a> MentionPickerView<'a> { pub fn new( img_cache: &'a mut Images, ndb: &'a Ndb, @@ -38,8 +40,8 @@ impl<'a> SearchResultsView<'a> { } } - fn show(&mut self, ui: &mut egui::Ui, width: f32) -> SearchResultsResponse { - let mut search_results_selection = None; + fn show(&mut self, ui: &mut egui::Ui, width: f32) -> MentionPickerResponse { + let mut selection = None; ui.vertical(|ui| { for (i, res) in self.results.iter().enumerate() { let profile = match self.ndb.get_profile_by_pubkey(self.txn, res) { @@ -54,16 +56,16 @@ impl<'a> SearchResultsView<'a> { .add(user_result(&profile, self.img_cache, i, width)) .clicked() { - search_results_selection = Some(i) + selection = Some(i) } } }); - SearchResultsResponse::SelectResult(search_results_selection) + MentionPickerResponse::SelectResult(selection) } - pub fn show_in_rect(&mut self, rect: egui::Rect, ui: &mut egui::Ui) -> SearchResultsResponse { - let widget_id = ui.id().with("search_results"); + pub fn show_in_rect(&mut self, rect: egui::Rect, ui: &mut egui::Ui) -> MentionPickerResponse { + let widget_id = ui.id().with("mention_results"); let area_resp = egui::Area::new(widget_id) .order(egui::Order::Foreground) .fixed_pos(rect.left_top()) @@ -72,10 +74,10 @@ impl<'a> SearchResultsView<'a> { let inner_margin_size = 8.0; egui::Frame::NONE .fill(ui.visuals().panel_fill) - .inner_margin(inner_margin_size) .show(ui, |ui| { let width = rect.width() - (2.0 * inner_margin_size); + ui.allocate_space(vec2(ui.available_width(), inner_margin_size)); let close_button_resp = { let close_button_size = 16.0; let (close_section_rect, _) = ui.allocate_exact_size( @@ -95,16 +97,16 @@ impl<'a> SearchResultsView<'a> { .inner }; - ui.add_space(8.0); + ui.allocate_space(vec2(ui.available_width(), inner_margin_size)); let scroll_resp = ScrollArea::vertical() - .max_width(width) + .max_width(rect.width()) .auto_shrink(Vec2b::FALSE) .show(ui, |ui| self.show(ui, width)); ui.advance_cursor_after_rect(rect); if close_button_resp { - SearchResultsResponse::DeleteMention + MentionPickerResponse::DeleteMention } else { scroll_resp.inner } @@ -128,7 +130,18 @@ fn user_result<'a>( let spacing = 8.0; let body_font_size = get_font_size(ui.ctx(), &NotedeckTextStyle::Body); - let helper = AnimationHelper::new(ui, ("user_result", index), vec2(width, max_image)); + let animation_rect = { + let max_width = ui.available_width(); + let extra_width = (max_width - width) / 2.0; + let left = ui.cursor().left(); + let (rect, _) = + ui.allocate_exact_size(vec2(width + extra_width, max_image), egui::Sense::click()); + + let (_, right) = rect.split_left_right_at_x(left + extra_width); + right + }; + + let helper = AnimationHelper::new_from_rect(ui, ("user_result", index), animation_rect); let icon_rect = { let r = helper.get_animation_rect(); diff --git a/crates/notedeck_columns/src/ui/mod.rs b/crates/notedeck_columns/src/ui/mod.rs index 46f9a882..93e68a6d 100644 --- a/crates/notedeck_columns/src/ui/mod.rs +++ b/crates/notedeck_columns/src/ui/mod.rs @@ -5,13 +5,13 @@ pub mod column; pub mod configure_deck; pub mod edit_deck; pub mod images; +pub mod mentions_picker; pub mod note; pub mod post; pub mod preview; pub mod profile; pub mod relay; pub mod search; -pub mod search_results; pub mod settings; pub mod side_panel; pub mod support; diff --git a/crates/notedeck_columns/src/ui/note/post.rs b/crates/notedeck_columns/src/ui/note/post.rs index 78a8a6cd..6c556539 100644 --- a/crates/notedeck_columns/src/ui/note/post.rs +++ b/crates/notedeck_columns/src/ui/note/post.rs @@ -2,7 +2,7 @@ use crate::draft::{Draft, Drafts, MentionHint}; #[cfg(not(target_os = "android"))] use crate::media_upload::{nostrbuild_nip96_upload, MediaPath}; use crate::post::{downcast_post_buffer, MentionType, NewPost}; -use crate::ui::search_results::SearchResultsView; +use crate::ui::mentions_picker::MentionPickerView; use crate::ui::{self, Preview, PreviewConfig}; use crate::Result; @@ -218,6 +218,7 @@ impl<'a, 'd> PostView<'a, 'd> { out.response } + // Displays the mention picker and handles when one is selected. fn show_mention_hints( &mut self, txn: &nostrdb::Transaction, @@ -273,7 +274,7 @@ impl<'a, 'd> PostView<'a, 'd> { return; }; - let resp = SearchResultsView::new( + let resp = MentionPickerView::new( self.note_context.img_cache, self.note_context.ndb, txn, @@ -281,26 +282,35 @@ impl<'a, 'd> PostView<'a, 'd> { ) .show_in_rect(hint_rect, ui); + let mut selection_made = None; match resp { - ui::search_results::SearchResultsResponse::SelectResult(selection) => { + ui::mentions_picker::MentionPickerResponse::SelectResult(selection) => { if let Some(hint_index) = selection { if let Some(pk) = res.get(hint_index) { let record = self.note_context.ndb.get_profile_by_pubkey(txn, pk); - self.draft.buffer.select_mention_and_replace_name( - mention.index, - get_display_name(record.ok().as_ref()).name(), - Pubkey::new(**pk), - ); + if let Some(made_selection) = + self.draft.buffer.select_mention_and_replace_name( + mention.index, + get_display_name(record.ok().as_ref()).name(), + Pubkey::new(**pk), + ) + { + selection_made = Some(made_selection); + } self.draft.cur_mention_hint = None; } } } - ui::search_results::SearchResultsResponse::DeleteMention => { + ui::mentions_picker::MentionPickerResponse::DeleteMention => { self.draft.buffer.delete_mention(mention.index) } } + + if let Some(selection) = selection_made { + selection.process(ui.ctx(), textedit_output); + } } fn focused(&self, ui: &egui::Ui) -> bool { diff --git a/crates/notedeck_columns/src/ui/search/mod.rs b/crates/notedeck_columns/src/ui/search/mod.rs index b08aafab..0b66b3d2 100644 --- a/crates/notedeck_columns/src/ui/search/mod.rs +++ b/crates/notedeck_columns/src/ui/search/mod.rs @@ -19,7 +19,7 @@ mod state; pub use state::{FocusState, SearchQueryState, SearchState}; -use super::search_results::{SearchResultsResponse, SearchResultsView}; +use super::mentions_picker::{MentionPickerResponse, MentionPickerView}; pub struct SearchView<'a, 'd> { query: &'a mut SearchQueryState, @@ -76,7 +76,7 @@ impl<'a, 'd> SearchView<'a, 'd> { break 's; }; - let search_res = SearchResultsView::new( + let search_res = MentionPickerView::new( self.note_context.img_cache, self.note_context.ndb, self.txn, @@ -85,7 +85,7 @@ impl<'a, 'd> SearchView<'a, 'd> { .show_in_rect(ui.available_rect_before_wrap(), ui); search_action = match search_res { - SearchResultsResponse::SelectResult(Some(index)) => { + MentionPickerResponse::SelectResult(Some(index)) => { let Some(pk_bytes) = results.get(index) else { break 's; }; @@ -103,8 +103,8 @@ impl<'a, 'd> SearchView<'a, 'd> { new_search_text: format!("@{username}"), }) } - SearchResultsResponse::DeleteMention => Some(SearchAction::CloseMention), - SearchResultsResponse::SelectResult(None) => break 's, + MentionPickerResponse::DeleteMention => Some(SearchAction::CloseMention), + MentionPickerResponse::SelectResult(None) => break 's, }; } SearchState::PerformSearch(search_type) => {