use crate::{ accounts::{render_accounts_route, AccountsAction, AccountsResponse}, app::{get_active_columns_mut, get_decks_mut}, column::ColumnsAction, deck_state::DeckState, decks::{Deck, DecksAction, DecksCache}, options::AppOptions, profile::{ProfileAction, SaveProfileChanges}, repost::RepostAction, route::{Route, Router, SingletonRouter}, subscriptions::Subscriptions, timeline::{ kind::ListKind, route::{render_thread_route, render_timeline_route}, TimelineCache, TimelineKind, }, ui::{ self, add_column::render_add_column_routes, column::NavTitle, configure_deck::ConfigureDeckView, edit_deck::{EditDeckResponse, EditDeckView}, note::{custom_zap::CustomZapView, NewPostAction, PostAction, PostType}, profile::EditProfileView, repost::RepostDecisionView, search::{FocusState, SearchView}, settings::SettingsAction, support::SupportView, wallet::{get_default_zap_state, WalletAction, WalletState, WalletView}, RelayView, SettingsView, }, Damus, }; use egui::scroll_area::ScrollAreaOutput; use egui_nav::{ Nav, NavAction, NavResponse, NavUiType, Percent, PopupResponse, PopupSheet, RouteResponse, }; use enostr::ProfileState; use nostrdb::{Filter, Ndb, Transaction}; use notedeck::{ get_current_default_msats, tr, ui::is_narrow, Accounts, AppContext, NoteAction, NoteContext, RelayAction, }; use notedeck_ui::NoteOptions; use tracing::error; /// The result of processing a nav response pub enum ProcessNavResult { SwitchOccurred, PfpClicked, } impl ProcessNavResult { pub fn switch_occurred(&self) -> bool { matches!(self, Self::SwitchOccurred) } } #[allow(clippy::enum_variant_names)] pub enum RenderNavAction { Back, RemoveColumn, /// The response when the user interacts with a pfp in the nav header PfpClicked, PostAction(NewPostAction), NoteAction(NoteAction), ProfileAction(ProfileAction), SwitchingAction(SwitchingAction), WalletAction(WalletAction), RelayAction(RelayAction), SettingsAction(SettingsAction), RepostAction(RepostAction), } pub enum SwitchingAction { Accounts(AccountsAction), Columns(ColumnsAction), Decks(crate::decks::DecksAction), } impl SwitchingAction { /// process the action, and return whether switching occured pub fn process( &self, timeline_cache: &mut TimelineCache, decks_cache: &mut DecksCache, ctx: &mut AppContext<'_>, subs: &mut Subscriptions, ui_ctx: &egui::Context, ) -> bool { match &self { SwitchingAction::Accounts(account_action) => match account_action { AccountsAction::Switch(switch_action) => { { let txn = Transaction::new(ctx.ndb).expect("txn"); ctx.accounts.select_account( &switch_action.switch_to, ctx.ndb, &txn, ctx.pool, ui_ctx, ); let contacts_sub = ctx.accounts.get_subs().contacts.remote.clone(); // this is cringe but we're gonna get a new sub manager soon... subs.subs.insert( contacts_sub, crate::subscriptions::SubKind::FetchingContactList(TimelineKind::List( ListKind::Contact(*ctx.accounts.selected_account_pubkey()), )), ); } if switch_action.switching_to_new { decks_cache.add_deck_default(ctx, timeline_cache, switch_action.switch_to); } // pop nav after switch get_active_columns_mut(ctx.i18n, ctx.accounts, decks_cache) .column_mut(switch_action.source_column) .router_mut() .go_back(); } AccountsAction::Remove(to_remove) => 's: { if !ctx .accounts .remove_account(to_remove, ctx.ndb, ctx.pool, ui_ctx) { break 's; } decks_cache.remove(ctx.i18n, to_remove, timeline_cache, ctx.ndb, ctx.pool); } }, SwitchingAction::Columns(columns_action) => match *columns_action { ColumnsAction::Remove(index) => { let kinds_to_pop = get_active_columns_mut(ctx.i18n, ctx.accounts, decks_cache) .delete_column(index); for kind in &kinds_to_pop { if let Err(err) = timeline_cache.pop(kind, ctx.ndb, ctx.pool) { error!("error popping timeline: {err}"); } } } ColumnsAction::Switch(from, to) => { get_active_columns_mut(ctx.i18n, ctx.accounts, decks_cache).move_col(from, to); } }, SwitchingAction::Decks(decks_action) => match *decks_action { DecksAction::Switch(index) => { get_decks_mut(ctx.i18n, ctx.accounts, decks_cache).set_active(index) } DecksAction::Removing(index) => { get_decks_mut(ctx.i18n, ctx.accounts, decks_cache).remove_deck( index, timeline_cache, ctx.ndb, ctx.pool, ); } }, } true } } impl From for RenderNavAction { fn from(post_action: PostAction) -> Self { match post_action { PostAction::QuotedNoteAction(note_action) => Self::NoteAction(note_action), PostAction::NewPostAction(new_post) => Self::PostAction(new_post), } } } impl From for RenderNavAction { fn from(post_action: NewPostAction) -> Self { Self::PostAction(post_action) } } impl From for RenderNavAction { fn from(note_action: NoteAction) -> RenderNavAction { Self::NoteAction(note_action) } } enum NotedeckNavResponse { Popup(Box>>), Nav(Box>>), } pub struct RenderNavResponse { column: usize, response: NotedeckNavResponse, } impl RenderNavResponse { #[allow(private_interfaces)] pub fn new(column: usize, response: NotedeckNavResponse) -> Self { RenderNavResponse { column, response } } pub fn can_take_drag_from(&self) -> Vec { match &self.response { NotedeckNavResponse::Popup(_) => Vec::new(), // TODO(kernelkind): upgrade once popup supports drag ids NotedeckNavResponse::Nav(nav_response) => nav_response.can_take_drag_from.clone(), } } #[must_use = "Make sure to save columns if result is true"] #[profiling::function] pub fn process_render_nav_response( self, app: &mut Damus, ctx: &mut AppContext<'_>, ui: &mut egui::Ui, ) -> Option { match self.response { NotedeckNavResponse::Popup(nav_action) => { process_popup_resp(*nav_action, app, ctx, ui, self.column) } NotedeckNavResponse::Nav(nav_response) => { process_nav_resp(app, ctx, ui, *nav_response, self.column) } } } } fn process_popup_resp( action: PopupResponse>, app: &mut Damus, ctx: &mut AppContext<'_>, ui: &mut egui::Ui, col: usize, ) -> Option { let mut process_result: Option = None; if let Some(nav_action) = action.response { process_result = process_render_nav_action(app, ctx, ui, col, nav_action); } if let Some(NavAction::Returned(_)) = action.action { let column = app.columns_mut(ctx.i18n, ctx.accounts).column_mut(col); column.sheet_router.clear(); } else if let Some(NavAction::Navigating) = action.action { let column = app.columns_mut(ctx.i18n, ctx.accounts).column_mut(col); column.sheet_router.navigating = false; } process_result } fn process_nav_resp( app: &mut Damus, ctx: &mut AppContext<'_>, ui: &mut egui::Ui, response: NavResponse>, col: usize, ) -> Option { let mut process_result: Option = None; if let Some(action) = response.response.or(response.title_response) { // start returning when we're finished posting process_result = process_render_nav_action(app, ctx, ui, col, action); } if let Some(action) = response.action { match action { NavAction::Returned(return_type) => { let r = app .columns_mut(ctx.i18n, ctx.accounts) .column_mut(col) .router_mut() .pop(); if let Some(Route::Timeline(kind)) = &r { if let Err(err) = app.timeline_cache.pop(kind, ctx.ndb, ctx.pool) { error!("popping timeline had an error: {err} for {:?}", kind); } }; if let Some(Route::Thread(selection)) = &r { app.threads .close(ctx.ndb, ctx.pool, selection, return_type, col); } // we should remove profile state once we've returned if let Some(Route::EditProfile(pk)) = &r { app.view_state.pubkey_to_profile_state.remove(pk); } process_result = Some(ProcessNavResult::SwitchOccurred); } NavAction::Navigated => { let cur_router = app .columns_mut(ctx.i18n, ctx.accounts) .column_mut(col) .router_mut(); cur_router.navigating = false; if cur_router.is_replacing() { cur_router.remove_previous_routes(); } process_result = Some(ProcessNavResult::SwitchOccurred); } NavAction::Dragging => {} NavAction::Returning(_) => {} NavAction::Resetting => {} NavAction::Navigating => { // explicitly update the edit profile state when navigating handle_navigating_edit_profile(ctx.ndb, ctx.accounts, app, col); } } } process_result } /// We are navigating to edit profile, prepare the profile state /// if we don't have it fn handle_navigating_edit_profile(ndb: &Ndb, accounts: &Accounts, app: &mut Damus, col: usize) { let pk = { let Route::EditProfile(pk) = app.columns(accounts).column(col).router().top() else { return; }; if app.view_state.pubkey_to_profile_state.contains_key(pk) { return; } pk.to_owned() }; let txn = Transaction::new(ndb).expect("txn"); app.view_state.pubkey_to_profile_state.insert(pk, { let filter = Filter::new_with_capacity(1) .kinds([0]) .authors([pk.bytes()]) .build(); if let Ok(results) = ndb.query(&txn, &[filter], 1) { if let Some(result) = results.first() { tracing::debug!( "refreshing profile state for edit view: {}", result.note.content() ); ProfileState::from_note_contents(result.note.content()) } else { ProfileState::default() } } else { ProfileState::default() } }); } pub enum RouterAction { GoBack, /// We clicked on a pfp in a route. We currently don't carry any /// information about the pfp since we only use it for toggling the /// chrome atm PfpClicked, RouteTo(Route, RouterType), CloseSheetThenRoute(Route), Overlay { route: Route, make_new: bool, }, } pub enum RouterType { Sheet, Stack, } fn go_back(stack: &mut Router, sheet: &mut SingletonRouter) { if sheet.route().is_some() { sheet.go_back(); } else { stack.go_back(); } } impl RouterAction { pub fn process( self, stack_router: &mut Router, sheet_router: &mut SingletonRouter, ) -> Option { match self { RouterAction::GoBack => { go_back(stack_router, sheet_router); None } RouterAction::PfpClicked => { if stack_router.routes().len() == 1 { // if we're at the top level and we click a profile pic, // bubble it up so that it can be handled by the chrome // to open the sidebar Some(ProcessNavResult::PfpClicked) } else { // Otherwise just execute a back action go_back(stack_router, sheet_router); None } } RouterAction::RouteTo(route, router_type) => match router_type { RouterType::Sheet => { sheet_router.route_to(route); None } RouterType::Stack => { stack_router.route_to(route); None } }, RouterAction::Overlay { route, make_new } => { if make_new { stack_router.route_to_overlaid_new(route); } else { stack_router.route_to_overlaid(route); } None } RouterAction::CloseSheetThenRoute(route) => { sheet_router.go_back(); sheet_router.after_action = Some(route); None } } } pub fn route_to(route: Route) -> Self { RouterAction::RouteTo(route, RouterType::Stack) } pub fn route_to_sheet(route: Route) -> Self { RouterAction::RouteTo(route, RouterType::Sheet) } } fn process_render_nav_action( app: &mut Damus, ctx: &mut AppContext<'_>, ui: &mut egui::Ui, col: usize, action: RenderNavAction, ) -> Option { let router_action = match action { RenderNavAction::Back => Some(RouterAction::GoBack), RenderNavAction::PfpClicked => Some(RouterAction::PfpClicked), RenderNavAction::RemoveColumn => { let kinds_to_pop = app.columns_mut(ctx.i18n, ctx.accounts).delete_column(col); for kind in &kinds_to_pop { if let Err(err) = app.timeline_cache.pop(kind, ctx.ndb, ctx.pool) { error!("error popping timeline: {err}"); } } return Some(ProcessNavResult::SwitchOccurred); } RenderNavAction::PostAction(new_post_action) => { let txn = Transaction::new(ctx.ndb).expect("txn"); match new_post_action.execute(ctx.ndb, &txn, ctx.pool, &mut app.drafts) { Err(err) => tracing::error!("Error executing post action: {err}"), Ok(_) => tracing::debug!("Post action executed"), } Some(RouterAction::GoBack) } RenderNavAction::NoteAction(note_action) => { let txn = Transaction::new(ctx.ndb).expect("txn"); crate::actionbar::execute_and_process_note_action( note_action, ctx.ndb, get_active_columns_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache), col, &mut app.timeline_cache, &mut app.threads, ctx.note_cache, ctx.pool, &txn, ctx.unknown_ids, ctx.accounts, ctx.global_wallet, ctx.zaps, ctx.img_cache, &mut app.view_state, ui, ) } RenderNavAction::SwitchingAction(switching_action) => { if switching_action.process( &mut app.timeline_cache, &mut app.decks_cache, ctx, &mut app.subscriptions, ui.ctx(), ) { return Some(ProcessNavResult::SwitchOccurred); } else { return None; } } RenderNavAction::ProfileAction(profile_action) => { profile_action.process_profile_action(ctx.ndb, ctx.pool, ctx.accounts) } RenderNavAction::WalletAction(wallet_action) => { wallet_action.process(ctx.accounts, ctx.global_wallet) } RenderNavAction::RelayAction(action) => { ctx.accounts .process_relay_action(ui.ctx(), ctx.pool, action); None } RenderNavAction::SettingsAction(action) => { action.process_settings_action(app, ctx.settings, ctx.i18n, ctx.img_cache, ui.ctx()) } RenderNavAction::RepostAction(action) => { action.process(ctx.ndb, &ctx.accounts.get_selected_account().key, ctx.pool) } }; if let Some(action) = router_action { let cols = get_active_columns_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache).column_mut(col); let router = &mut cols.router; let sheet_router = &mut cols.sheet_router; action.process(router, sheet_router) } else { None } } fn render_nav_body( ui: &mut egui::Ui, app: &mut Damus, ctx: &mut AppContext, top: &Route, depth: usize, col: usize, inner_rect: egui::Rect, ) -> BodyResponse { let mut note_context = NoteContext { ndb: ctx.ndb, accounts: ctx.accounts, img_cache: ctx.img_cache, note_cache: ctx.note_cache, zaps: ctx.zaps, pool: ctx.pool, job_pool: ctx.job_pool, unknown_ids: ctx.unknown_ids, clipboard: ctx.clipboard, i18n: ctx.i18n, global_wallet: ctx.global_wallet, }; match top { Route::Timeline(kind) => { // did something request scroll to top for the selection column? let scroll_to_top = app .decks_cache .selected_column_index(ctx.accounts) .is_some_and(|ind| ind == col) && app.options.contains(AppOptions::ScrollToTop); let resp = render_timeline_route( &mut app.timeline_cache, kind, col, app.note_options, depth, ui, &mut note_context, &mut app.jobs, scroll_to_top, ); app.timeline_cache.set_fresh(kind); // always clear the scroll_to_top request if scroll_to_top { app.options.remove(AppOptions::ScrollToTop); } resp } Route::Thread(selection) => render_thread_route( &mut app.threads, selection, col, app.note_options, ui, &mut note_context, &mut app.jobs, ), Route::Accounts(amr) => { let resp = render_accounts_route( ui, ctx, &mut app.jobs, &mut app.view_state.login, &mut app.onboarding, &mut app.view_state.follow_packs, *amr, ); resp.map_output_maybe(|action| match action { AccountsResponse::ViewProfile(pubkey) => { Some(RenderNavAction::NoteAction(NoteAction::Profile(pubkey))) } AccountsResponse::Account(accounts_route_response) => { let mut action = accounts_route_response.process(ctx, app, col); let txn = Transaction::new(ctx.ndb).expect("txn"); action.process_action(ctx.unknown_ids, ctx.ndb, &txn); action .accounts_action .map(|f| RenderNavAction::SwitchingAction(SwitchingAction::Accounts(f))) } }) } Route::Relays => RelayView::new(ctx.pool, &mut app.view_state.id_string_map, ctx.i18n) .ui(ui) .map_output(RenderNavAction::RelayAction), Route::Settings => SettingsView::new( ctx.settings.get_settings_mut(), &mut note_context, &mut app.note_options, &mut app.jobs, ) .ui(ui) .map_output(RenderNavAction::SettingsAction), Route::Reply(id) => { let txn = if let Ok(txn) = Transaction::new(ctx.ndb) { txn } else { ui.label(tr!( note_context.i18n, "Reply to unknown note", "Error message when reply note cannot be found" )); return BodyResponse::none(); }; let note = if let Ok(note) = ctx.ndb.get_note_by_id(&txn, id.bytes()) { note } else { ui.label(tr!( note_context.i18n, "Reply to unknown note", "Error message when reply note cannot be found" )); return BodyResponse::none(); }; let Some(poster) = ctx.accounts.selected_filled() else { return BodyResponse::none(); }; let resp = { let draft = app.drafts.reply_mut(note.id()); let mut options = app.note_options; options.set(NoteOptions::Wide, false); ui::PostReplyView::new( &mut note_context, poster, draft, ¬e, inner_rect, options, &mut app.jobs, col, ) .show(ui) }; resp.map_output_maybe(|o| Some(o.action?.into())) } Route::Quote(id) => { let txn = Transaction::new(ctx.ndb).expect("txn"); let note = if let Ok(note) = ctx.ndb.get_note_by_id(&txn, id.bytes()) { note } else { ui.label(tr!( note_context.i18n, "Quote of unknown note", "Error message when quote note cannot be found" )); return BodyResponse::none(); }; let Some(poster) = ctx.accounts.selected_filled() else { return BodyResponse::none(); }; let draft = app.drafts.quote_mut(note.id()); let response = crate::ui::note::QuoteRepostView::new( &mut note_context, poster, draft, ¬e, inner_rect, app.note_options, &mut app.jobs, col, ) .show(ui); response.map_output_maybe(|o| Some(o.action?.into())) } Route::ComposeNote => { let Some(kp) = ctx.accounts.get_selected_account().key.to_full() else { return BodyResponse::none(); }; let draft = app.drafts.compose_mut(); let txn = Transaction::new(ctx.ndb).expect("txn"); let post_response = ui::PostView::new( &mut note_context, draft, PostType::New, kp, inner_rect, app.note_options, &mut app.jobs, ) .ui(&txn, ui); post_response.map_output_maybe(|o| Some(o.action?.into())) } Route::AddColumn(route) => { render_add_column_routes(ui, app, ctx, col, route); BodyResponse::none() } Route::Support => { SupportView::new(&mut app.support, ctx.i18n).show(ui); BodyResponse::none() } Route::Search => { let id = ui.id().with(("search", depth, col)); let navigating = get_active_columns_mut(note_context.i18n, ctx.accounts, &mut app.decks_cache) .column(col) .router() .navigating; let search_buffer = app.view_state.searches.entry(id).or_default(); let txn = Transaction::new(ctx.ndb).expect("txn"); if navigating { search_buffer.focus_state = FocusState::Navigating } else if search_buffer.focus_state == FocusState::Navigating { // we're not navigating but our last search buffer state // says we were navigating. This means that navigating has // stopped. Let's make sure to focus the input field search_buffer.focus_state = FocusState::ShouldRequestFocus; } SearchView::new( &txn, app.note_options, search_buffer, &mut note_context, &mut app.jobs, ) .show(ui) .map_output(RenderNavAction::NoteAction) } Route::NewDeck => { let id = ui.id().with("new-deck"); let new_deck_state = app.view_state.id_to_deck_state.entry(id).or_default(); let mut resp = None; if let Some(config_resp) = ConfigureDeckView::new(new_deck_state, ctx.i18n).ui(ui) { let cur_acc = ctx.accounts.selected_account_pubkey(); app.decks_cache .add_deck(*cur_acc, Deck::new(config_resp.icon, config_resp.name)); // set new deck as active let cur_index = get_decks_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache) .decks() .len() - 1; resp = Some(RenderNavAction::SwitchingAction(SwitchingAction::Decks( DecksAction::Switch(cur_index), ))); new_deck_state.clear(); get_active_columns_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache) .get_selected_router() .go_back(); } BodyResponse::output(resp) } Route::EditDeck(index) => { let mut action = None; let cur_deck = get_decks_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache) .decks_mut() .get_mut(*index) .expect("index wasn't valid"); let id = ui .id() .with(("edit-deck", ctx.accounts.selected_account_pubkey(), index)); let deck_state = app .view_state .id_to_deck_state .entry(id) .or_insert_with(|| DeckState::from_deck(cur_deck)); if let Some(resp) = EditDeckView::new(deck_state, ctx.i18n).ui(ui) { match resp { EditDeckResponse::Edit(configure_deck_response) => { cur_deck.edit(configure_deck_response); } EditDeckResponse::Delete => { action = Some(RenderNavAction::SwitchingAction(SwitchingAction::Decks( DecksAction::Removing(*index), ))); } } get_active_columns_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache) .get_selected_router() .go_back(); } BodyResponse::output(action) } Route::EditProfile(pubkey) => { let Some(kp) = ctx.accounts.get_full(pubkey) else { error!("Pubkey in EditProfile route did not have an nsec attached in Accounts"); return BodyResponse::none(); }; let Some(state) = app.view_state.pubkey_to_profile_state.get_mut(kp.pubkey) else { tracing::error!( "No profile state when navigating to EditProfile... was handle_navigating_edit_profile not called?" ); return BodyResponse::none(); }; EditProfileView::new(ctx.i18n, state, ctx.img_cache, ctx.clipboard) .ui(ui) .map_output_maybe(|save| { if save { app.view_state .pubkey_to_profile_state .get(kp.pubkey) .map(|state| { RenderNavAction::ProfileAction(ProfileAction::SaveChanges( SaveProfileChanges::new(kp.to_full(), state.clone()), )) }) } else { None } }) } Route::Wallet(wallet_type) => { let state = match wallet_type { notedeck::WalletType::Auto => 's: { if let Some(cur_acc_wallet) = ctx.accounts.get_selected_wallet_mut() { let default_zap_state = get_default_zap_state(&mut cur_acc_wallet.default_zap); break 's WalletState::Wallet { wallet: &mut cur_acc_wallet.wallet, default_zap_state, can_create_local_wallet: false, }; } let Some(wallet) = &mut ctx.global_wallet.wallet else { break 's WalletState::NoWallet { state: &mut ctx.global_wallet.ui_state, show_local_only: true, }; }; let default_zap_state = get_default_zap_state(&mut wallet.default_zap); WalletState::Wallet { wallet: &mut wallet.wallet, default_zap_state, can_create_local_wallet: true, } } notedeck::WalletType::Local => 's: { let cur_acc = ctx.accounts.get_selected_wallet_mut(); let Some(wallet) = cur_acc else { break 's WalletState::NoWallet { state: &mut ctx.global_wallet.ui_state, show_local_only: false, }; }; let default_zap_state = get_default_zap_state(&mut wallet.default_zap); WalletState::Wallet { wallet: &mut wallet.wallet, default_zap_state, can_create_local_wallet: false, } } }; BodyResponse::output(WalletView::new(state, ctx.i18n, ctx.clipboard).ui(ui)) .map_output(RenderNavAction::WalletAction) } Route::CustomizeZapAmount(target) => { let txn = Transaction::new(ctx.ndb).expect("txn"); let default_msats = get_current_default_msats(ctx.accounts, ctx.global_wallet); BodyResponse::output( CustomZapView::new( ctx.i18n, ctx.img_cache, ctx.ndb, &txn, &target.zap_recipient, default_msats, ) .ui(ui), ) .map_output(|msats| { get_active_columns_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache) .column_mut(col) .router_mut() .go_back(); RenderNavAction::NoteAction(NoteAction::Zap(notedeck::ZapAction::Send( notedeck::note::ZapTargetAmount { target: target.clone(), specified_msats: Some(msats), }, ))) }) } Route::RepostDecision(note_id) => { BodyResponse::output(RepostDecisionView::new(note_id).show(ui)) .map_output(RenderNavAction::RepostAction) } } } pub struct BodyResponse { pub drag_id: Option, // the id which was used for dragging. pub output: Option, } impl BodyResponse { pub fn none() -> Self { Self { drag_id: None, output: None, } } pub fn scroll(output: ScrollAreaOutput>) -> Self { Self { drag_id: Some(Self::scroll_output_to_drag_id(output.id)), output: output.inner, } } pub fn set_scroll_id(&mut self, output: &ScrollAreaOutput>) { self.drag_id = Some(Self::scroll_output_to_drag_id(output.id)); } pub fn output(output: Option) -> Self { Self { drag_id: None, output, } } pub fn set_output(&mut self, output: R) { self.output = Some(output); } /// The id of an `egui::ScrollAreaOutput` /// Should use `Self::scroll` when possible pub fn scroll_raw(mut self, id: egui::Id) -> Self { self.drag_id = Some(Self::scroll_output_to_drag_id(id)); self } /// The id which is directly used for dragging pub fn set_drag_id_raw(&mut self, id: egui::Id) { self.drag_id = Some(id); } fn scroll_output_to_drag_id(id: egui::Id) -> egui::Id { id.with("area") } pub fn map_output(self, f: impl FnOnce(R) -> S) -> BodyResponse { BodyResponse { drag_id: self.drag_id, output: self.output.map(f), } } pub fn map_output_maybe(self, f: impl FnOnce(R) -> Option) -> BodyResponse { BodyResponse { drag_id: self.drag_id, output: self.output.and_then(f), } } pub fn maybe_map_output(self, f: impl FnOnce(Option) -> S) -> BodyResponse { BodyResponse { drag_id: self.drag_id, output: Some(f(self.output)), } } /// insert the contents of the new BodyResponse if they are empty in Self pub fn insert(&mut self, body: BodyResponse) { self.drag_id = self.drag_id.or(body.drag_id); if self.output.is_none() { self.output = body.output; } } } #[must_use = "RenderNavResponse must be handled by calling .process_render_nav_response(..)"] #[profiling::function] pub fn render_nav( col: usize, inner_rect: egui::Rect, app: &mut Damus, ctx: &mut AppContext<'_>, ui: &mut egui::Ui, ) -> RenderNavResponse { let narrow = is_narrow(ui.ctx()); if let Some(sheet_route) = app .columns(ctx.accounts) .column(col) .sheet_router .route() .clone() { let navigating = app .columns(ctx.accounts) .column(col) .sheet_router .navigating; let returning = app.columns(ctx.accounts).column(col).sheet_router.returning; let bg_route = app .columns(ctx.accounts) .column(col) .router() .routes() .last() .cloned(); if let Some(bg_route) = bg_route { let resp = PopupSheet::new(&bg_route, &sheet_route) .id_source(egui::Id::new(("nav", col))) .navigating(navigating) .returning(returning) .with_split_percent_from_top(Percent::new(35).expect("35 <= 100")) .show_mut(ui, |ui, typ, route| match typ { NavUiType::Title => NavTitle::new( ctx.ndb, ctx.img_cache, get_active_columns_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache), std::slice::from_ref(route), col, ctx.i18n, ) .show_move_button(!narrow) .show_delete_button(!narrow) .show(ui), NavUiType::Body => { render_nav_body(ui, app, ctx, route, 1, col, inner_rect).output } }); return RenderNavResponse::new(col, NotedeckNavResponse::Popup(Box::new(resp))); } }; let routes = app .columns(ctx.accounts) .column(col) .router() .routes() .clone(); let nav = Nav::new(&routes).id_source(egui::Id::new(("nav", col))); let nav_response = nav .navigating( app.columns_mut(ctx.i18n, ctx.accounts) .column_mut(col) .router_mut() .navigating, ) .returning( app.columns_mut(ctx.i18n, ctx.accounts) .column_mut(col) .router_mut() .returning, ) .show_mut(ui, |ui, render_type, nav| match render_type { NavUiType::Title => { let action = NavTitle::new( ctx.ndb, ctx.img_cache, get_active_columns_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache), nav.routes(), col, ctx.i18n, ) .show_move_button(!narrow) .show_delete_button(!narrow) .show(ui); RouteResponse { response: action, can_take_drag_from: Vec::new(), } } NavUiType::Body => { let resp = if let Some(top) = nav.routes().last() { render_nav_body(ui, app, ctx, top, nav.routes().len(), col, inner_rect) } else { BodyResponse::none() }; RouteResponse { response: resp.output, can_take_drag_from: resp.drag_id.map(|d| vec![d]).unwrap_or(Vec::new()), } } }); RenderNavResponse::new(col, NotedeckNavResponse::Nav(Box::new(nav_response))) }