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>
This commit is contained in:
William Casarin
2025-07-30 16:22:16 -07:00
7 changed files with 136 additions and 62 deletions

33
Cargo.lock generated
View File

@@ -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"

View File

@@ -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" }

View File

@@ -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());

View File

@@ -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<usize>),
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();

View File

@@ -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;

View File

@@ -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);
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 {

View File

@@ -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) => {