notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

commit ba76b20ad2eef666945566e65c353bba7e8568d3
parent 0fc8e70180a334fd62745380c0431a114678b027
Author: William Casarin <jb55@jb55.com>
Date:   Wed, 30 Jul 2025 16:22:16 -0700

Merge tagging fixes from kernel

Fixes the following:
1. space added after mention
2. can scroll the mention picker
3. don't lose focus of textedit after mention selection

kernelkind (6):
      rename `SearchResultsView` => `MentionPickerView`
      fix scroll regression
      mention-picker: re-add spacing from inner_margin
      mentions: don't lose focus after select mention
      TMP: update egui for better TextInputState handling
      insert space after mention selection

Fixes: https://github.com/damus-io/notedeck/issues/985
Fixes: https://github.com/damus-io/notedeck/issues/728
Fixes: https://github.com/damus-io/notedeck/issues/986

Signed-off-by: William Casarin <jb55@jb55.com>

Diffstat:
MCargo.lock | 33++++++++++++++++++++-------------
MCargo.toml | 12++++++------
Mcrates/notedeck_columns/src/post.rs | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
Acrates/notedeck_columns/src/ui/mentions_picker.rs | 183+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/notedeck_columns/src/ui/mod.rs | 2+-
Mcrates/notedeck_columns/src/ui/note/post.rs | 28+++++++++++++++++++---------
Mcrates/notedeck_columns/src/ui/search/mod.rs | 10+++++-----
Dcrates/notedeck_columns/src/ui/search_results.rs | 170-------------------------------------------------------------------------------
8 files changed, 292 insertions(+), 218 deletions(-)

diff --git 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" @@ -5349,6 +5350,12 @@ dependencies = [ ] [[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" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git 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 @@ -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<MentionSelectedResponse> { + 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/mentions_picker.rs b/crates/notedeck_columns/src/ui/mentions_picker.rs @@ -0,0 +1,183 @@ +use egui::{vec2, FontId, Layout, Pos2, Rect, ScrollArea, UiBuilder, Vec2b}; +use nostrdb::{Ndb, ProfileRecord, Transaction}; +use notedeck::{ + fonts::get_font_size, name::get_display_name, profile::get_profile_url, Images, + NotedeckTextStyle, +}; +use notedeck_ui::{ + anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE}, + widgets::x_button, + ProfilePic, +}; +use tracing::error; + +/// 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 MentionPickerResponse { + SelectResult(Option<usize>), + DeleteMention, +} + +impl<'a> MentionPickerView<'a> { + pub fn new( + img_cache: &'a mut Images, + ndb: &'a Ndb, + txn: &'a Transaction, + results: &'a Vec<&'a [u8; 32]>, + ) -> Self { + Self { + ndb, + txn, + img_cache, + results, + } + } + + 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) { + Ok(rec) => rec, + Err(e) => { + error!("Error fetching profile for pubkey {:?}: {e}", res); + return; + } + }; + + if ui + .add(user_result(&profile, self.img_cache, i, width)) + .clicked() + { + selection = Some(i) + } + } + }); + + MentionPickerResponse::SelectResult(selection) + } + + 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()) + .constrain_to(rect) + .show(ui.ctx(), |ui| { + let inner_margin_size = 8.0; + egui::Frame::NONE + .fill(ui.visuals().panel_fill) + .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( + vec2(width, close_button_size), + egui::Sense::hover(), + ); + let (_, button_rect) = close_section_rect.split_left_right_at_x( + close_section_rect.right() - close_button_size, + ); + let button_resp = ui.allocate_rect(button_rect, egui::Sense::click()); + ui.allocate_new_ui( + UiBuilder::new() + .max_rect(close_section_rect) + .layout(Layout::right_to_left(egui::Align::Center)), + |ui| ui.add(x_button(button_resp.rect)).clicked(), + ) + .inner + }; + + ui.allocate_space(vec2(ui.available_width(), inner_margin_size)); + + let scroll_resp = ScrollArea::vertical() + .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 { + MentionPickerResponse::DeleteMention + } else { + scroll_resp.inner + } + }) + .inner + }); + + area_resp.inner + } +} + +fn user_result<'a>( + profile: &'a ProfileRecord<'_>, + cache: &'a mut Images, + index: usize, + width: f32, +) -> impl egui::Widget + 'a { + move |ui: &mut egui::Ui| -> egui::Response { + let min_img_size = 48.0; + let max_image = min_img_size * ICON_EXPANSION_MULTIPLE; + let spacing = 8.0; + let body_font_size = get_font_size(ui.ctx(), &NotedeckTextStyle::Body); + + 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(); + let mut center = r.center(); + center.x = r.left() + (max_image / 2.0); + let size = helper.scale_1d_pos(min_img_size); + Rect::from_center_size(center, vec2(size, size)) + }; + + let pfp_resp = ui.put( + icon_rect, + &mut ProfilePic::new(cache, get_profile_url(Some(profile))) + .size(helper.scale_1d_pos(min_img_size)), + ); + + let name_font = FontId::new( + helper.scale_1d_pos(body_font_size), + NotedeckTextStyle::Body.font_family(), + ); + let painter = ui.painter_at(helper.get_animation_rect()); + let name_galley = painter.layout( + get_display_name(Some(profile)).name().to_owned(), + name_font, + ui.visuals().text_color(), + width, + ); + + let galley_pos = { + let right_top = pfp_resp.rect.right_top(); + let galley_pos_y = pfp_resp.rect.center().y - (name_galley.rect.height() / 2.0); + Pos2::new(right_top.x + spacing, galley_pos_y) + }; + + painter.galley(galley_pos, name_galley, ui.visuals().text_color()); + ui.advance_cursor_after_rect(helper.get_animation_rect()); + + pfp_resp.union(helper.take_animation_response()) + } +} diff --git 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 @@ -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 @@ -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) => { diff --git a/crates/notedeck_columns/src/ui/search_results.rs b/crates/notedeck_columns/src/ui/search_results.rs @@ -1,170 +0,0 @@ -use egui::{vec2, FontId, Layout, Pos2, Rect, ScrollArea, UiBuilder, Vec2b}; -use nostrdb::{Ndb, ProfileRecord, Transaction}; -use notedeck::{ - fonts::get_font_size, name::get_display_name, profile::get_profile_url, Images, - NotedeckTextStyle, -}; -use notedeck_ui::{ - anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE}, - widgets::x_button, - ProfilePic, -}; -use tracing::error; - -pub struct SearchResultsView<'a> { - ndb: &'a Ndb, - txn: &'a Transaction, - img_cache: &'a mut Images, - results: &'a Vec<&'a [u8; 32]>, -} - -pub enum SearchResultsResponse { - SelectResult(Option<usize>), - DeleteMention, -} - -impl<'a> SearchResultsView<'a> { - pub fn new( - img_cache: &'a mut Images, - ndb: &'a Ndb, - txn: &'a Transaction, - results: &'a Vec<&'a [u8; 32]>, - ) -> Self { - Self { - ndb, - txn, - img_cache, - results, - } - } - - fn show(&mut self, ui: &mut egui::Ui, width: f32) -> SearchResultsResponse { - let mut search_results_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) { - Ok(rec) => rec, - Err(e) => { - error!("Error fetching profile for pubkey {:?}: {e}", res); - return; - } - }; - - if ui - .add(user_result(&profile, self.img_cache, i, width)) - .clicked() - { - search_results_selection = Some(i) - } - } - }); - - SearchResultsResponse::SelectResult(search_results_selection) - } - - pub fn show_in_rect(&mut self, rect: egui::Rect, ui: &mut egui::Ui) -> SearchResultsResponse { - let widget_id = ui.id().with("search_results"); - let area_resp = egui::Area::new(widget_id) - .order(egui::Order::Foreground) - .fixed_pos(rect.left_top()) - .constrain_to(rect) - .show(ui.ctx(), |ui| { - 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); - - let close_button_resp = { - let close_button_size = 16.0; - let (close_section_rect, _) = ui.allocate_exact_size( - vec2(width, close_button_size), - egui::Sense::hover(), - ); - let (_, button_rect) = close_section_rect.split_left_right_at_x( - close_section_rect.right() - close_button_size, - ); - let button_resp = ui.allocate_rect(button_rect, egui::Sense::click()); - ui.allocate_new_ui( - UiBuilder::new() - .max_rect(close_section_rect) - .layout(Layout::right_to_left(egui::Align::Center)), - |ui| ui.add(x_button(button_resp.rect)).clicked(), - ) - .inner - }; - - ui.add_space(8.0); - - let scroll_resp = ScrollArea::vertical() - .max_width(width) - .auto_shrink(Vec2b::FALSE) - .show(ui, |ui| self.show(ui, width)); - ui.advance_cursor_after_rect(rect); - - if close_button_resp { - SearchResultsResponse::DeleteMention - } else { - scroll_resp.inner - } - }) - .inner - }); - - area_resp.inner - } -} - -fn user_result<'a>( - profile: &'a ProfileRecord<'_>, - cache: &'a mut Images, - index: usize, - width: f32, -) -> impl egui::Widget + 'a { - move |ui: &mut egui::Ui| -> egui::Response { - let min_img_size = 48.0; - let max_image = min_img_size * ICON_EXPANSION_MULTIPLE; - 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 icon_rect = { - let r = helper.get_animation_rect(); - let mut center = r.center(); - center.x = r.left() + (max_image / 2.0); - let size = helper.scale_1d_pos(min_img_size); - Rect::from_center_size(center, vec2(size, size)) - }; - - let pfp_resp = ui.put( - icon_rect, - &mut ProfilePic::new(cache, get_profile_url(Some(profile))) - .size(helper.scale_1d_pos(min_img_size)), - ); - - let name_font = FontId::new( - helper.scale_1d_pos(body_font_size), - NotedeckTextStyle::Body.font_family(), - ); - let painter = ui.painter_at(helper.get_animation_rect()); - let name_galley = painter.layout( - get_display_name(Some(profile)).name().to_owned(), - name_font, - ui.visuals().text_color(), - width, - ); - - let galley_pos = { - let right_top = pfp_resp.rect.right_top(); - let galley_pos_y = pfp_resp.rect.center().y - (name_galley.rect.height() / 2.0); - Pos2::new(right_top.x + spacing, galley_pos_y) - }; - - painter.galley(galley_pos, name_galley, ui.visuals().text_color()); - ui.advance_cursor_after_rect(helper.get_animation_rect()); - - pfp_resp.union(helper.take_animation_response()) - } -}