notedeck

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

commit 09dc101c1bc206f5d655c079d183b24b4e70feb6
parent 0fc8e70180a334fd62745380c0431a114678b027
Author: kernelkind <kernelkind@gmail.com>
Date:   Sat, 26 Jul 2025 16:20:53 -0400

rename `SearchResultsView` => `MentionPickerView`

Signed-off-by: kernelkind <kernelkind@gmail.com>

Diffstat:
Acrates/notedeck_columns/src/ui/mentions_picker.rs | 172+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/notedeck_columns/src/ui/mod.rs | 2+-
Mcrates/notedeck_columns/src/ui/note/post.rs | 8++++----
Mcrates/notedeck_columns/src/ui/search/mod.rs | 10+++++-----
Dcrates/notedeck_columns/src/ui/search_results.rs | 170-------------------------------------------------------------------------------
5 files changed, 182 insertions(+), 180 deletions(-)

diff --git a/crates/notedeck_columns/src/ui/mentions_picker.rs b/crates/notedeck_columns/src/ui/mentions_picker.rs @@ -0,0 +1,172 @@ +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) + .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 { + 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 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()) + } +} 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; @@ -273,7 +273,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, @@ -282,7 +282,7 @@ impl<'a, 'd> PostView<'a, 'd> { .show_in_rect(hint_rect, ui); 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); @@ -297,7 +297,7 @@ impl<'a, 'd> PostView<'a, 'd> { } } - ui::search_results::SearchResultsResponse::DeleteMention => { + ui::mentions_picker::MentionPickerResponse::DeleteMention => { self.draft.buffer.delete_mention(mention.index) } } 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()) - } -}