notedeck

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

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:
MCargo.lock | 2+-
MCargo.toml | 2+-
Mcrates/notedeck/src/persist/settings_handler.rs | 12++++++++++++
Mcrates/notedeck_columns/src/app.rs | 46++++++++++++++++++++++++++++++++++++++++++++--
Mcrates/notedeck_columns/src/nav.rs | 50++++++++++++++++++++++++++++++++++++++++++++++----
Mcrates/notedeck_columns/src/post.rs | 5-----
Mcrates/notedeck_columns/src/route.rs | 34+++++++++++++++++++++++++++-------
Mcrates/notedeck_columns/src/ui/search/mod.rs | 9++++++++-
Mcrates/notedeck_columns/src/ui/settings.rs | 20++++++++++++++++++++
Mcrates/notedeck_columns/src/ui/side_panel.rs | 56+++++++++++++++++++++++++++++++++++++++++++++++++++++---
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 }