commit 94efc54d06413a39d5c1cd08374f56c0fa3675ef
parent eac5d41e3cb1587c65d1fe29cbc23df0ec302ed5
Author: William Casarin <jb55@jb55.com>
Date: Mon, 10 Nov 2025 12:45:47 -0800
Merge disable egui-nav animations by martii #1192
Martti Malmi (6):
handle mouse back button
fwd function (not working for profile views)
fix fwd nav to profile, profile avatar at sidebar bottom
allow to disable transition animation
fix build errors
use egui-nav with animate_transitions support
Diffstat:
10 files changed, 212 insertions(+), 24 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -1555,7 +1555,7 @@ dependencies = [
[[package]]
name = "egui_nav"
version = "0.2.0"
-source = "git+https://github.com/damus-io/egui-nav?rev=15304033930e4cb8ccae9551b439fb958732fc66#15304033930e4cb8ccae9551b439fb958732fc66"
+source = "git+https://github.com/damus-io/egui-nav?rev=4a54a6c7c34243e4bcfdeb37cc15de3536811719#4a54a6c7c34243e4bcfdeb37cc15de3536811719"
dependencies = [
"bitflags 2.9.1",
"egui",
diff --git a/Cargo.toml b/Cargo.toml
@@ -28,7 +28,7 @@ egui = { version = "0.31.1", features = ["serde"] }
egui-wgpu = "0.31.1"
egui_extras = { version = "0.31.1", features = ["all_loaders"] }
egui-winit = { version = "0.31.1", features = ["android-game-activity", "clipboard"] }
-egui_nav = { git = "https://github.com/damus-io/egui-nav", rev = "15304033930e4cb8ccae9551b439fb958732fc66" }
+egui_nav = { git = "https://github.com/damus-io/egui-nav", rev = "4a54a6c7c34243e4bcfdeb37cc15de3536811719" }
egui_tabs = { git = "https://github.com/damus-io/egui-tabs", rev = "6eb91740577b374a8a6658c09c9a4181299734d0" }
#egui_virtual_list = "0.6.0"
egui_virtual_list = { git = "https://github.com/jb55/hello_egui", rev = "a66b6794f5e707a2f4109633770e02b02fb722e1" }
diff --git a/crates/notedeck/src/persist/settings_handler.rs b/crates/notedeck/src/persist/settings_handler.rs
@@ -36,6 +36,12 @@ pub struct Settings {
pub show_source_client: String,
pub show_replies_newest_first: bool,
pub note_body_font_size: f32,
+ #[serde(default = "default_animate_nav_transitions")]
+ pub animate_nav_transitions: bool,
+}
+
+fn default_animate_nav_transitions() -> bool {
+ true
}
impl Default for Settings {
@@ -47,6 +53,7 @@ impl Default for Settings {
show_source_client: DEFAULT_SHOW_SOURCE_CLIENT.to_string(),
show_replies_newest_first: DEFAULT_SHOW_REPLIES_NEWEST_FIRST,
note_body_font_size: DEFAULT_NOTE_BODY_FONT_SIZE,
+ animate_nav_transitions: default_animate_nav_transitions(),
}
}
}
@@ -191,6 +198,11 @@ impl SettingsHandler {
self.try_save_settings();
}
+ pub fn set_animate_nav_transitions(&mut self, value: bool) {
+ self.get_settings_mut().animate_nav_transitions = value;
+ self.try_save_settings();
+ }
+
pub fn update_batch<F>(&mut self, update_fn: F)
where
F: FnOnce(&mut Settings),
diff --git a/crates/notedeck_columns/src/app.rs b/crates/notedeck_columns/src/app.rs
@@ -62,9 +62,16 @@ pub struct Damus {
/// keep track of follow packs
pub onboarding: Onboarding,
+
+ /// Track which column is hovered for mouse back/forward navigation
+ hovered_column: Option<usize>,
}
-fn handle_egui_events(input: &egui::InputState, columns: &mut Columns) {
+fn handle_egui_events(
+ input: &egui::InputState,
+ columns: &mut Columns,
+ hovered_column: Option<usize>,
+) {
for event in &input.raw.events {
match event {
egui::Event::Key { key, pressed, .. } if *pressed => match key {
@@ -89,6 +96,30 @@ fn handle_egui_events(input: &egui::InputState, columns: &mut Columns) {
_ => {}
},
+ egui::Event::PointerButton {
+ button: egui::PointerButton::Extra1,
+ pressed: true,
+ ..
+ } => {
+ if let Some(col_idx) = hovered_column {
+ columns.column_mut(col_idx).router_mut().go_back();
+ } else {
+ columns.get_selected_router().go_back();
+ }
+ }
+
+ egui::Event::PointerButton {
+ button: egui::PointerButton::Extra2,
+ pressed: true,
+ ..
+ } => {
+ if let Some(col_idx) = hovered_column {
+ columns.column_mut(col_idx).router_mut().go_forward();
+ } else {
+ columns.get_selected_router().go_forward();
+ }
+ }
+
egui::Event::InsetsChanged => {
tracing::debug!("insets have changed!");
}
@@ -106,7 +137,7 @@ fn try_process_event(
) -> Result<()> {
let current_columns =
get_active_columns_mut(app_ctx.i18n, app_ctx.accounts, &mut damus.decks_cache);
- ctx.input(|i| handle_egui_events(i, current_columns));
+ ctx.input(|i| handle_egui_events(i, current_columns, damus.hovered_column));
let ctx2 = ctx.clone();
let wakeup = move || {
@@ -533,6 +564,7 @@ impl Damus {
jobs,
threads,
onboarding: Onboarding::default(),
+ hovered_column: None,
}
}
@@ -584,6 +616,7 @@ impl Damus {
jobs: JobsCache::default(),
threads: Threads::default(),
onboarding: Onboarding::default(),
+ hovered_column: None,
}
}
@@ -841,6 +874,8 @@ fn timelines_view(
ctx.accounts.get_selected_account(),
&app.decks_cache,
ctx.i18n,
+ ctx.ndb,
+ ctx.img_cache,
)
.show(ui);
@@ -876,6 +911,8 @@ fn timelines_view(
);
});
+ app.hovered_column = None;
+
for col_index in 0..num_cols {
strip.cell(|ui| {
let rect = ui.available_rect_before_wrap();
@@ -889,6 +926,11 @@ fn timelines_view(
can_take_drag_from.extend(resp.can_take_drag_from());
responses.push(resp);
+ // Track hovered column for mouse back/forward navigation
+ if ui.rect_contains_pointer(rect) {
+ app.hovered_column = Some(col_index);
+ }
+
// vertical line
ui.painter()
.vline(rect.right(), rect.y_range(), v_line_stroke);
diff --git a/crates/notedeck_columns/src/nav.rs b/crates/notedeck_columns/src/nav.rs
@@ -36,11 +36,11 @@ use egui::scroll_area::ScrollAreaOutput;
use egui_nav::{
Nav, NavAction, NavResponse, NavUiType, PopupResponse, PopupSheet, RouteResponse, Split,
};
-use enostr::ProfileState;
+use enostr::{ProfileState, RelayPool};
use nostrdb::{Filter, Ndb, Transaction};
use notedeck::{
- get_current_default_msats, tr, ui::is_narrow, Accounts, AppContext, NoteAction, NoteContext,
- RelayAction,
+ get_current_default_msats, tr, ui::is_narrow, Accounts, AppContext, NoteAction, NoteCache,
+ NoteContext, RelayAction,
};
use notedeck_ui::NoteOptions;
use tracing::error;
@@ -299,6 +299,16 @@ fn process_nav_resp(
}
NavAction::Navigated => {
+ handle_navigating_edit_profile(ctx.ndb, ctx.accounts, app, col);
+ handle_navigating_timeline(
+ ctx.ndb,
+ ctx.note_cache,
+ ctx.pool,
+ ctx.accounts,
+ app,
+ col,
+ );
+
let cur_router = app
.columns_mut(ctx.i18n, ctx.accounts)
.column_mut(col)
@@ -315,8 +325,15 @@ fn process_nav_resp(
NavAction::Returning(_) => {}
NavAction::Resetting => {}
NavAction::Navigating => {
- // explicitly update the edit profile state when navigating
handle_navigating_edit_profile(ctx.ndb, ctx.accounts, app, col);
+ handle_navigating_timeline(
+ ctx.ndb,
+ ctx.note_cache,
+ ctx.pool,
+ ctx.accounts,
+ app,
+ col,
+ );
}
}
}
@@ -362,6 +379,30 @@ fn handle_navigating_edit_profile(ndb: &Ndb, accounts: &Accounts, app: &mut Damu
});
}
+fn handle_navigating_timeline(
+ ndb: &Ndb,
+ note_cache: &mut NoteCache,
+ pool: &mut RelayPool,
+ accounts: &Accounts,
+ app: &mut Damus,
+ col: usize,
+) {
+ let kind = {
+ let Route::Timeline(kind) = app.columns(accounts).column(col).router().top() else {
+ return;
+ };
+
+ if app.timeline_cache.get(kind).is_some() {
+ return;
+ }
+
+ kind.to_owned()
+ };
+
+ let txn = Transaction::new(ndb).expect("txn");
+ app.timeline_cache.open(ndb, note_cache, &txn, pool, &kind);
+}
+
pub enum RouterAction {
GoBack,
/// We clicked on a pfp in a route. We currently don't carry any
@@ -1118,6 +1159,7 @@ pub fn render_nav(
.router_mut()
.returning,
)
+ .animate_transitions(ctx.settings.get_settings_mut().animate_nav_transitions)
.show_mut(ui, |ui, render_type, nav| match render_type {
NavUiType::Title => {
let action = NavTitle::new(
diff --git a/crates/notedeck_columns/src/post.rs b/crates/notedeck_columns/src/post.rs
@@ -281,11 +281,6 @@ impl MentionSelectedResponse {
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(
diff --git a/crates/notedeck_columns/src/route.rs b/crates/notedeck_columns/src/route.rs
@@ -389,6 +389,7 @@ pub struct Router<R: Clone> {
pub returning: bool,
pub navigating: bool,
replacing: bool,
+ forward_stack: Vec<R>,
// An overlay captures a range of routes where only one will persist when going back, the most recent added
overlay_ranges: Vec<Range<usize>>,
@@ -407,12 +408,14 @@ impl<R: Clone> Router<R> {
returning,
navigating,
replacing,
+ forward_stack: Vec::new(),
overlay_ranges: Vec::new(),
}
}
pub fn route_to(&mut self, route: R) {
self.navigating = true;
+ self.forward_stack.clear();
self.routes.push(route);
}
@@ -454,31 +457,48 @@ impl<R: Clone> Router<R> {
self.prev().cloned()
}
+ pub fn go_forward(&mut self) -> bool {
+ if let Some(route) = self.forward_stack.pop() {
+ self.navigating = true;
+ self.routes.push(route);
+ true
+ } else {
+ false
+ }
+ }
+
/// Pop a route, should only be called on a NavRespose::Returned reseponse
pub fn pop(&mut self) -> Option<R> {
if self.routes.len() == 1 {
return None;
}
- 's: {
+ let is_overlay = 's: {
let Some(last_range) = self.overlay_ranges.last_mut() else {
- break 's;
+ break 's false;
};
if last_range.end != self.routes.len() {
- break 's;
+ break 's false;
}
if last_range.end - 1 <= last_range.start {
self.overlay_ranges.pop();
- break 's;
+ } else {
+ last_range.end -= 1;
}
- last_range.end -= 1;
- }
+ true
+ };
self.returning = false;
- self.routes.pop()
+ let popped = self.routes.pop();
+ if !is_overlay {
+ if let Some(ref route) = popped {
+ self.forward_stack.push(route.clone());
+ }
+ }
+ popped
}
pub fn remove_previous_routes(&mut self) {
diff --git a/crates/notedeck_columns/src/ui/search/mod.rs b/crates/notedeck_columns/src/ui/search/mod.rs
@@ -1,4 +1,4 @@
-use egui::{vec2, Align, Color32, CornerRadius, RichText, Stroke, TextEdit};
+use egui::{vec2, Align, Color32, CornerRadius, Key, RichText, Stroke, TextEdit};
use enostr::{NoteId, Pubkey};
use state::TypingType;
@@ -318,6 +318,13 @@ fn search_box(
.frame(false),
);
+ if response.has_focus()
+ && ui
+ .input(|i| i.key_pressed(Key::ArrowUp) || i.key_pressed(Key::ArrowDown))
+ {
+ response.surrender_focus();
+ }
+
input_context(ui, &response, clipboard, input, PasteBehavior::Append);
let mut requested_focus = false;
diff --git a/crates/notedeck_columns/src/ui/settings.rs b/crates/notedeck_columns/src/ui/settings.rs
@@ -35,6 +35,7 @@ pub enum SettingsAction {
SetLocale(LanguageIdentifier),
SetRepliestNewestFirst(bool),
SetNoteBodyFontSize(f32),
+ SetAnimateNavTransitions(bool),
OpenRelays,
OpenCacheFolder,
ClearCacheFolder,
@@ -89,6 +90,9 @@ impl SettingsAction {
settings.set_note_body_font_size(size);
}
+ Self::SetAnimateNavTransitions(value) => {
+ settings.set_animate_nav_transitions(value);
+ }
}
route_action
}
@@ -474,6 +478,22 @@ impl<'a> SettingsView<'a> {
));
}
});
+
+ ui.horizontal_wrapped(|ui| {
+ ui.label(richtext_small("Animate view transitions:"));
+
+ if ui
+ .toggle_value(
+ &mut self.settings.animate_nav_transitions,
+ RichText::new("On").text_style(NotedeckTextStyle::Small.text_style()),
+ )
+ .changed()
+ {
+ action = Some(SettingsAction::SetAnimateNavTransitions(
+ self.settings.animate_nav_transitions,
+ ));
+ }
+ });
});
action
diff --git a/crates/notedeck_columns/src/ui/side_panel.rs b/crates/notedeck_columns/src/ui/side_panel.rs
@@ -15,7 +15,7 @@ use crate::{
use notedeck::{tr, Accounts, Localization, UserAccount};
use notedeck_ui::{
anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE},
- app_images, colors, View,
+ app_images, colors, ProfilePic, View,
};
use super::configure_deck::deck_icon;
@@ -27,6 +27,8 @@ pub struct DesktopSidePanel<'a> {
selected_account: &'a UserAccount,
decks_cache: &'a DecksCache,
i18n: &'a mut Localization,
+ ndb: &'a nostrdb::Ndb,
+ img_cache: &'a mut notedeck::Images,
}
impl View for DesktopSidePanel<'_> {
@@ -45,6 +47,7 @@ pub enum SidePanelAction {
SwitchDeck(usize),
EditDeck(usize),
Wallet,
+ ProfileAvatar,
}
pub struct SidePanelResponse {
@@ -63,11 +66,15 @@ impl<'a> DesktopSidePanel<'a> {
selected_account: &'a UserAccount,
decks_cache: &'a DecksCache,
i18n: &'a mut Localization,
+ ndb: &'a nostrdb::Ndb,
+ img_cache: &'a mut notedeck::Images,
) -> Self {
Self {
selected_account,
decks_cache,
i18n,
+ ndb,
+ img_cache,
}
}
@@ -122,13 +129,46 @@ impl<'a> DesktopSidePanel<'a> {
ui.add_space(8.0);
let add_deck_resp = ui.add(add_deck_button(self.i18n));
+ let avatar_size = 40.0;
+ let bottom_padding = 8.0;
+ let avatar_section_height = avatar_size + bottom_padding;
+
+ let available_for_decks = ui.available_height() - avatar_section_height;
+
let decks_inner = ScrollArea::vertical()
- .max_height(ui.available_height() - (3.0 * (ICON_WIDTH + 12.0)))
+ .max_height(available_for_decks)
.show(ui, |ui| {
show_decks(ui, self.decks_cache, self.selected_account)
})
.inner;
+ let remaining = ui.available_height();
+ if remaining > avatar_section_height {
+ ui.add_space(remaining - avatar_section_height);
+ }
+
+ let txn = nostrdb::Transaction::new(self.ndb).ok();
+ let profile_url = if let Some(ref txn) = txn {
+ if let Ok(profile) = self
+ .ndb
+ .get_profile_by_pubkey(txn, self.selected_account.key.pubkey.bytes())
+ {
+ notedeck::profile::get_profile_url(Some(&profile))
+ } else {
+ notedeck::profile::no_pfp_url()
+ }
+ } else {
+ notedeck::profile::no_pfp_url()
+ };
+
+ let pfp_resp = ui
+ .add(
+ &mut ProfilePic::new(self.img_cache, profile_url)
+ .size(avatar_size)
+ .sense(egui::Sense::click()),
+ )
+ .on_hover_cursor(egui::CursorIcon::PointingHand);
+
/*
if expand_resp.clicked() {
Some(InnerResponse::new(
@@ -136,7 +176,9 @@ impl<'a> DesktopSidePanel<'a> {
expand_resp,
))
*/
- if compose_resp.clicked() {
+ if pfp_resp.clicked() {
+ Some(InnerResponse::new(SidePanelAction::ProfileAvatar, pfp_resp))
+ } else if compose_resp.clicked() {
Some(InnerResponse::new(
SidePanelAction::ComposeNote,
compose_resp,
@@ -299,6 +341,14 @@ impl<'a> DesktopSidePanel<'a> {
router.route_to(Route::Wallet(notedeck::WalletType::Auto));
}
+ SidePanelAction::ProfileAvatar => {
+ let pubkey = accounts.get_selected_account().key.pubkey;
+ if router.routes().iter().any(|r| r == &Route::profile(pubkey)) {
+ router.go_back();
+ } else {
+ router.route_to(Route::profile(pubkey));
+ }
+ }
}
switching_response
}