columns: enable toolbar scroll to top

Fixes: https://github.com/damus-io/notedeck/issues/969
Signed-off-by: William Casarin <jb55@jb55.com>
This commit is contained in:
William Casarin
2025-07-15 13:29:59 -07:00
parent 074472eec9
commit ac22fc7072
7 changed files with 98 additions and 24 deletions

View File

@@ -6,7 +6,9 @@ use egui::{vec2, Button, Color32, Label, Layout, Rect, RichText, ThemePreference
use egui_extras::{Size, StripBuilder}; use egui_extras::{Size, StripBuilder};
use nostrdb::{ProfileRecord, Transaction}; use nostrdb::{ProfileRecord, Transaction};
use notedeck::{App, AppAction, AppContext, NotedeckTextStyle, UserAccount, WalletType}; use notedeck::{App, AppAction, AppContext, NotedeckTextStyle, UserAccount, WalletType};
use notedeck_columns::{timeline::kind::ListKind, timeline::TimelineKind, Damus}; use notedeck_columns::{
column::SelectionResult, timeline::kind::ListKind, timeline::TimelineKind, Damus,
};
use notedeck_dave::{Dave, DaveAvatar}; use notedeck_dave::{Dave, DaveAvatar};
use notedeck_ui::{app_images, AnimationHelper, ProfilePic}; use notedeck_ui::{app_images, AnimationHelper, ProfilePic};
@@ -61,11 +63,26 @@ impl ChromePanelAction {
fn columns_switch(ctx: &AppContext, chrome: &mut Chrome, kind: &TimelineKind) { fn columns_switch(ctx: &AppContext, chrome: &mut Chrome, kind: &TimelineKind) {
chrome.switch_to_columns(); chrome.switch_to_columns();
if let Some(active_columns) = chrome let Some(columns_app) = chrome.get_columns_app() else {
.get_columns() return;
.and_then(|cols| cols.decks_cache.active_columns_mut(ctx.accounts)) };
{
active_columns.select_by_kind(kind) if let Some(active_columns) = columns_app.decks_cache.active_columns_mut(ctx.accounts) {
match active_columns.select_by_kind(kind) {
SelectionResult::NewSelection(_index) => {
// great! no need to go to top yet
}
SelectionResult::AlreadySelected(_n) => {
// we already selected this, so scroll to top
columns_app.scroll_to_top();
}
SelectionResult::Failed => {
// oh no, something went wrong
// TODO(jb55): handle tab selection failure
}
}
} }
} }
@@ -73,7 +90,7 @@ impl ChromePanelAction {
chrome.switch_to_columns(); chrome.switch_to_columns();
if let Some(c) = chrome if let Some(c) = chrome
.get_columns() .get_columns_app()
.and_then(|columns| columns.decks_cache.selected_column_mut(ctx.accounts)) .and_then(|columns| columns.decks_cache.selected_column_mut(ctx.accounts))
{ {
if c.router().routes().iter().any(|r| r == &route) { if c.router().routes().iter().any(|r| r == &route) {
@@ -155,7 +172,7 @@ impl Chrome {
self.apps.push(app); self.apps.push(app);
} }
fn get_columns(&mut self) -> Option<&mut Damus> { fn get_columns_app(&mut self) -> Option<&mut Damus> {
for app in &mut self.apps { for app in &mut self.apps {
if let NotedeckApp::Columns(cols) = app { if let NotedeckApp::Columns(cols) = app {
return Some(cols); return Some(cols);
@@ -632,7 +649,7 @@ fn chrome_handle_app_action(
AppAction::Note(note_action) => { AppAction::Note(note_action) => {
chrome.switch_to_columns(); chrome.switch_to_columns();
let Some(columns) = chrome.get_columns() else { let Some(columns) = chrome.get_columns_app() else {
return; return;
}; };

View File

@@ -42,6 +42,18 @@ pub struct Columns {
pub selected: i32, pub selected: i32,
} }
/// When selecting columns, return what happened
pub enum SelectionResult {
/// We're already selecting that
AlreadySelected(usize),
/// New selection success!
NewSelection(usize),
/// Failed to make a selection
Failed,
}
impl Columns { impl Columns {
pub fn new() -> Self { pub fn new() -> Self {
Columns::default() Columns::default()
@@ -60,20 +72,25 @@ impl Columns {
/// Select the column based on the timeline kind. /// Select the column based on the timeline kind.
/// ///
/// TODO: add timeline if missing? /// TODO: add timeline if missing?
pub fn select_by_kind(&mut self, kind: &TimelineKind) { pub fn select_by_kind(&mut self, kind: &TimelineKind) -> SelectionResult {
for (i, col) in self.columns.iter().enumerate() { for (i, col) in self.columns.iter().enumerate() {
for route in col.router().routes() { for route in col.router().routes() {
if let Some(timeline) = route.timeline_id() { if let Some(timeline) = route.timeline_id() {
if timeline == kind { if timeline == kind {
tracing::info!("selecting {kind:?} column"); tracing::info!("selecting {kind:?} column");
self.select_column(i as i32); if self.selected as usize == i {
return; return SelectionResult::AlreadySelected(i);
} else {
self.select_column(i as i32);
return SelectionResult::NewSelection(i);
}
} }
} }
} }
} }
tracing::error!("failed to select {kind:?} column"); tracing::error!("failed to select {kind:?} column");
SelectionResult::Failed
} }
pub fn add_new_timeline_column( pub fn add_new_timeline_column(

View File

@@ -40,6 +40,10 @@ impl DecksCache {
self.active_columns(accounts).and_then(|ad| ad.selected()) self.active_columns(accounts).and_then(|ad| ad.selected())
} }
pub fn selected_column_index(&self, accounts: &notedeck::Accounts) -> Option<usize> {
self.active_columns(accounts).map(|ad| ad.selected as usize)
}
/// Gets a mutable reference to the active columns /// Gets a mutable reference to the active columns
pub fn active_columns_mut(&mut self, accounts: &notedeck::Accounts) -> Option<&mut Columns> { pub fn active_columns_mut(&mut self, accounts: &notedeck::Accounts) -> Option<&mut Columns> {
let account = accounts.get_selected_account(); let account = accounts.get_selected_account();

View File

@@ -8,7 +8,7 @@ pub mod actionbar;
pub mod app_creation; pub mod app_creation;
mod app_style; mod app_style;
mod args; mod args;
mod column; pub mod column;
mod deck_state; mod deck_state;
mod decks; mod decks;
mod draft; mod draft;

View File

@@ -4,6 +4,7 @@ use crate::{
column::ColumnsAction, column::ColumnsAction,
deck_state::DeckState, deck_state::DeckState,
decks::{Deck, DecksAction, DecksCache}, decks::{Deck, DecksAction, DecksCache},
options::AppOptions,
profile::{ProfileAction, SaveProfileChanges}, profile::{ProfileAction, SaveProfileChanges},
route::{Route, Router, SingletonRouter}, route::{Route, Router, SingletonRouter},
timeline::{ timeline::{
@@ -496,17 +497,34 @@ fn render_nav_body(
current_account_has_wallet: get_current_wallet(ctx.accounts, ctx.global_wallet).is_some(), current_account_has_wallet: get_current_wallet(ctx.accounts, ctx.global_wallet).is_some(),
}; };
match top { match top {
Route::Timeline(kind) => render_timeline_route( Route::Timeline(kind) => {
&mut app.timeline_cache, // did something request scroll to top for the selection column?
ctx.accounts, let scroll_to_top = app
kind, .decks_cache
col, .selected_column_index(ctx.accounts)
app.note_options, .is_some_and(|ind| ind == col)
depth, && app.options.contains(AppOptions::ScrollToTop);
ui,
&mut note_context, let nav_action = render_timeline_route(
&mut app.jobs, &mut app.timeline_cache,
), ctx.accounts,
kind,
col,
app.note_options,
depth,
ui,
&mut note_context,
&mut app.jobs,
scroll_to_top,
);
// always clear the scroll_to_top request
if scroll_to_top {
app.options.remove(AppOptions::ScrollToTop);
}
nav_action
}
Route::Thread(selection) => render_thread_route( Route::Thread(selection) => render_thread_route(
&mut app.threads, &mut app.threads,
ctx.accounts, ctx.accounts,

View File

@@ -20,6 +20,7 @@ pub fn render_timeline_route(
ui: &mut egui::Ui, ui: &mut egui::Ui,
note_context: &mut NoteContext, note_context: &mut NoteContext,
jobs: &mut JobsCache, jobs: &mut JobsCache,
scroll_to_top: bool,
) -> Option<RenderNavAction> { ) -> Option<RenderNavAction> {
match kind { match kind {
TimelineKind::List(_) TimelineKind::List(_)
@@ -39,6 +40,7 @@ pub fn render_timeline_route(
jobs, jobs,
col, col,
) )
.scroll_to_top(scroll_to_top)
.ui(ui); .ui(ui);
note_action.map(RenderNavAction::NoteAction) note_action.map(RenderNavAction::NoteAction)
@@ -69,6 +71,7 @@ pub fn render_timeline_route(
jobs, jobs,
col, col,
) )
.scroll_to_top(scroll_to_top)
.ui(ui); .ui(ui);
note_action.map(RenderNavAction::NoteAction) note_action.map(RenderNavAction::NoteAction)

View File

@@ -25,6 +25,7 @@ pub struct TimelineView<'a, 'd> {
cur_acc: &'a KeypairUnowned<'a>, cur_acc: &'a KeypairUnowned<'a>,
jobs: &'a mut JobsCache, jobs: &'a mut JobsCache,
col: usize, col: usize,
scroll_to_top: bool,
} }
impl<'a, 'd> TimelineView<'a, 'd> { impl<'a, 'd> TimelineView<'a, 'd> {
@@ -40,6 +41,7 @@ impl<'a, 'd> TimelineView<'a, 'd> {
col: usize, col: usize,
) -> Self { ) -> Self {
let reverse = false; let reverse = false;
let scroll_to_top = false;
TimelineView { TimelineView {
timeline_id, timeline_id,
timeline_cache, timeline_cache,
@@ -50,6 +52,7 @@ impl<'a, 'd> TimelineView<'a, 'd> {
cur_acc, cur_acc,
jobs, jobs,
col, col,
scroll_to_top,
} }
} }
@@ -65,9 +68,15 @@ impl<'a, 'd> TimelineView<'a, 'd> {
self.cur_acc, self.cur_acc,
self.jobs, self.jobs,
self.col, self.col,
self.scroll_to_top,
) )
} }
pub fn scroll_to_top(mut self, enable: bool) -> Self {
self.scroll_to_top = enable;
self
}
pub fn reversed(mut self) -> Self { pub fn reversed(mut self) -> Self {
self.reverse = true; self.reverse = true;
self self
@@ -86,6 +95,7 @@ fn timeline_ui(
cur_acc: &KeypairUnowned, cur_acc: &KeypairUnowned,
jobs: &mut JobsCache, jobs: &mut JobsCache,
col: usize, col: usize,
scroll_to_top: bool,
) -> Option<NoteAction> { ) -> Option<NoteAction> {
//padding(4.0, ui, |ui| ui.heading("Notifications")); //padding(4.0, ui, |ui| ui.heading("Notifications"));
/* /*
@@ -152,6 +162,11 @@ fn timeline_ui(
} }
} }
// chrome can ask to scroll to top as well via an app option
if scroll_to_top {
scroll_area = scroll_area.vertical_scroll_offset(0.0);
}
let scroll_output = scroll_area.show(ui, |ui| { let scroll_output = scroll_area.show(ui, |ui| {
let timeline = if let Some(timeline) = timeline_cache.timelines.get(timeline_id) { let timeline = if let Some(timeline) = timeline_cache.timelines.get(timeline_id) {
timeline timeline