commit 2cd627f5aa21076969ee7e8aab9184ee3dd0fe9c
parent cfa56ab056e1af20e6211dec880f64dc4d71b423
Author: William Casarin <jb55@jb55.com>
Date: Fri, 27 Feb 2026 11:06:39 -0800
app: split App trait into update() + render()
Split the App trait so Chrome can call background processing on ALL apps
every frame while only rendering the active one. This fixes relay
connections dying when switching between apps (e.g., Dave's PNS relay
pool timing out while Columns is active).
Chrome::update() now iterates all apps calling update(), while only the
active app gets render() called via the sidebar panel.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat:
16 files changed, 115 insertions(+), 95 deletions(-)
diff --git a/crates/notedeck/src/app.rs b/crates/notedeck/src/app.rs
@@ -34,7 +34,11 @@ pub enum AppAction {
}
pub trait App {
- fn update(&mut self, ctx: &mut AppContext<'_>, ui: &mut egui::Ui) -> AppResponse;
+ /// Background processing — called every frame for ALL apps.
+ fn update(&mut self, _ctx: &mut AppContext<'_>, _egui_ctx: &egui::Context) {}
+
+ /// UI rendering — called only for the active/visible app.
+ fn render(&mut self, ctx: &mut AppContext<'_>, ui: &mut egui::Ui) -> AppResponse;
}
#[derive(Default)]
@@ -104,20 +108,9 @@ fn render_notedeck(
app_ctx: &mut AppContext,
ctx: &egui::Context,
) {
+ app.borrow_mut().update(app_ctx, ctx);
main_panel(&ctx.style()).show(ctx, |ui| {
- app.borrow_mut().update(app_ctx, ui);
-
- // Move the screen up when we have a virtual keyboard
- // NOTE: actually, we only want to do this if the keyboard is covering the focused element?
- /*
- let keyboard_height = crate::platform::virtual_keyboard_height() as f32;
- if keyboard_height > 0.0 {
- ui.ctx().transform_layer_shapes(
- ui.layer_id(),
- egui::emath::TSTransform::from_translation(egui::Vec2::new(0.0, -(keyboard_height/2.0))),
- );
- }
- */
+ app.borrow_mut().render(app_ctx, ui);
});
}
diff --git a/crates/notedeck_chrome/src/app.rs b/crates/notedeck_chrome/src/app.rs
@@ -42,27 +42,52 @@ pub enum NotedeckApp {
impl notedeck::App for NotedeckApp {
#[profiling::function]
- fn update(&mut self, ctx: &mut AppContext, ui: &mut egui::Ui) -> AppResponse {
+ fn update(&mut self, ctx: &mut AppContext, egui_ctx: &egui::Context) {
match self {
- NotedeckApp::Dave(dave) => dave.update(ctx, ui),
- NotedeckApp::Columns(columns) => columns.update(ctx, ui),
+ NotedeckApp::Dave(dave) => dave.update(ctx, egui_ctx),
+ NotedeckApp::Columns(columns) => columns.update(ctx, egui_ctx),
#[cfg(feature = "notebook")]
- NotedeckApp::Notebook(notebook) => notebook.update(ctx, ui),
+ NotedeckApp::Notebook(notebook) => notebook.update(ctx, egui_ctx),
#[cfg(feature = "clndash")]
- NotedeckApp::ClnDash(clndash) => clndash.update(ctx, ui),
+ NotedeckApp::ClnDash(clndash) => clndash.update(ctx, egui_ctx),
#[cfg(feature = "messages")]
- NotedeckApp::Messages(dms) => dms.update(ctx, ui),
+ NotedeckApp::Messages(dms) => dms.update(ctx, egui_ctx),
#[cfg(feature = "dashboard")]
- NotedeckApp::Dashboard(db) => db.update(ctx, ui),
+ NotedeckApp::Dashboard(db) => db.update(ctx, egui_ctx),
#[cfg(feature = "nostrverse")]
- NotedeckApp::Nostrverse(nostrverse) => nostrverse.update(ctx, ui),
+ NotedeckApp::Nostrverse(nostrverse) => nostrverse.update(ctx, egui_ctx),
- NotedeckApp::Other(_name, other) => other.update(ctx, ui),
+ NotedeckApp::Other(_name, other) => other.update(ctx, egui_ctx),
+ }
+ }
+
+ #[profiling::function]
+ fn render(&mut self, ctx: &mut AppContext, ui: &mut egui::Ui) -> AppResponse {
+ match self {
+ NotedeckApp::Dave(dave) => dave.render(ctx, ui),
+ NotedeckApp::Columns(columns) => columns.render(ctx, ui),
+
+ #[cfg(feature = "notebook")]
+ NotedeckApp::Notebook(notebook) => notebook.render(ctx, ui),
+
+ #[cfg(feature = "clndash")]
+ NotedeckApp::ClnDash(clndash) => clndash.render(ctx, ui),
+
+ #[cfg(feature = "messages")]
+ NotedeckApp::Messages(dms) => dms.render(ctx, ui),
+
+ #[cfg(feature = "dashboard")]
+ NotedeckApp::Dashboard(db) => db.render(ctx, ui),
+
+ #[cfg(feature = "nostrverse")]
+ NotedeckApp::Nostrverse(nostrverse) => nostrverse.render(ctx, ui),
+
+ NotedeckApp::Other(_name, other) => other.render(ctx, ui),
}
}
}
diff --git a/crates/notedeck_chrome/src/chrome.rs b/crates/notedeck_chrome/src/chrome.rs
@@ -340,7 +340,7 @@ impl Chrome {
.inner
}
ChromeRoute::App => {
- let resp = self.apps[self.active as usize].update(app_ctx, ui);
+ let resp = self.apps[self.active as usize].render(app_ctx, ui);
if let Some(action) = resp.action {
chrome_handle_app_action(self, app_ctx, action, ui);
@@ -428,7 +428,15 @@ impl Chrome {
}
impl notedeck::App for Chrome {
- fn update(&mut self, ctx: &mut notedeck::AppContext, ui: &mut egui::Ui) -> AppResponse {
+ fn update(&mut self, ctx: &mut notedeck::AppContext, egui_ctx: &egui::Context) {
+ // Update ALL apps every frame so background processing
+ // (relay pools, subscriptions, etc.) stays alive
+ for app in &mut self.apps {
+ app.update(ctx, egui_ctx);
+ }
+ }
+
+ fn render(&mut self, ctx: &mut notedeck::AppContext, ui: &mut egui::Ui) -> AppResponse {
#[cfg(feature = "tracy")]
{
ui.ctx().request_repaint();
@@ -438,7 +446,6 @@ impl notedeck::App for Chrome {
action.process(ctx, self, ui);
self.nav.close();
}
- // TODO: unify this constant with the columns side panel width. ui crate?
AppResponse::none()
}
}
diff --git a/crates/notedeck_clndash/src/lib.rs b/crates/notedeck_clndash/src/lib.rs
@@ -60,18 +60,18 @@ struct CommChannel {
}
impl notedeck::App for ClnDash {
- fn update(&mut self, ctx: &mut AppContext<'_>, ui: &mut egui::Ui) -> AppResponse {
+ fn update(&mut self, ctx: &mut AppContext<'_>, _egui_ctx: &egui::Context) {
if !self.initialized {
self.connection_state = ConnectionState::Connecting;
-
self.setup_connection();
self.initialized = true;
}
self.process_events(ctx.ndb);
+ }
+ fn render(&mut self, ctx: &mut AppContext<'_>, ui: &mut egui::Ui) -> AppResponse {
self.show(ui, ctx);
-
AppResponse::none()
}
}
diff --git a/crates/notedeck_columns/src/app.rs b/crates/notedeck_columns/src/app.rs
@@ -1025,14 +1025,12 @@ fn timelines_view(
impl notedeck::App for Damus {
#[profiling::function]
- fn update(&mut self, ctx: &mut AppContext<'_>, ui: &mut egui::Ui) -> AppResponse {
- /*
- self.app
- .frame_history
- .on_new_frame(ctx.input(|i| i.time), frame.info().cpu_usage);
- */
-
- update_damus(self, ctx, ui.ctx());
+ fn update(&mut self, ctx: &mut AppContext<'_>, egui_ctx: &egui::Context) {
+ update_damus(self, ctx, egui_ctx);
+ }
+
+ #[profiling::function]
+ fn render(&mut self, ctx: &mut AppContext<'_>, ui: &mut egui::Ui) -> AppResponse {
render_damus(self, ctx, ui)
}
}
diff --git a/crates/notedeck_columns/src/ui/account_login_view.rs b/crates/notedeck_columns/src/ui/account_login_view.rs
@@ -180,9 +180,8 @@ mod preview {
}
impl App for AccountLoginPreview {
- fn update(&mut self, ctx: &mut AppContext<'_>, ui: &mut egui::Ui) -> AppResponse {
+ fn render(&mut self, ctx: &mut AppContext<'_>, ui: &mut egui::Ui) -> AppResponse {
AccountLoginView::new(&mut self.manager, ctx.clipboard, ctx.i18n).ui(ui);
-
AppResponse::none()
}
}
diff --git a/crates/notedeck_columns/src/ui/configure_deck.rs b/crates/notedeck_columns/src/ui/configure_deck.rs
@@ -341,9 +341,8 @@ mod preview {
}
impl App for ConfigureDeckPreview {
- fn update(&mut self, ctx: &mut AppContext<'_>, ui: &mut egui::Ui) -> AppResponse {
+ fn render(&mut self, ctx: &mut AppContext<'_>, ui: &mut egui::Ui) -> AppResponse {
ConfigureDeckView::new(&mut self.state, ctx.i18n).ui(ui);
-
AppResponse::none()
}
}
diff --git a/crates/notedeck_columns/src/ui/edit_deck.rs b/crates/notedeck_columns/src/ui/edit_deck.rs
@@ -75,7 +75,7 @@ mod preview {
}
impl App for EditDeckPreview {
- fn update(&mut self, ctx: &mut AppContext<'_>, ui: &mut egui::Ui) -> AppResponse {
+ fn render(&mut self, ctx: &mut AppContext<'_>, ui: &mut egui::Ui) -> AppResponse {
EditDeckView::new(&mut self.state, ctx.i18n).ui(ui);
AppResponse::none()
}
diff --git a/crates/notedeck_columns/src/ui/note/post.rs b/crates/notedeck_columns/src/ui/note/post.rs
@@ -891,7 +891,7 @@ mod preview {
}
impl App for PostPreview {
- fn update(&mut self, app: &mut AppContext<'_>, ui: &mut egui::Ui) -> AppResponse {
+ fn render(&mut self, app: &mut AppContext<'_>, ui: &mut egui::Ui) -> AppResponse {
let txn = Transaction::new(app.ndb).expect("txn");
let mut note_context = NoteContext {
ndb: app.ndb,
diff --git a/crates/notedeck_columns/src/ui/preview.rs b/crates/notedeck_columns/src/ui/preview.rs
@@ -22,8 +22,8 @@ impl PreviewApp {
}
impl notedeck::App for PreviewApp {
- fn update(&mut self, app_ctx: &mut notedeck::AppContext<'_>, ui: &mut egui::Ui) -> AppResponse {
- self.view.update(app_ctx, ui);
+ fn render(&mut self, app_ctx: &mut notedeck::AppContext<'_>, ui: &mut egui::Ui) -> AppResponse {
+ self.view.render(app_ctx, ui);
AppResponse::none()
}
}
diff --git a/crates/notedeck_dashboard/src/lib.rs b/crates/notedeck_dashboard/src/lib.rs
@@ -244,17 +244,18 @@ impl Default for Dashboard {
}
impl notedeck::App for Dashboard {
- fn update(&mut self, ctx: &mut AppContext<'_>, ui: &mut egui::Ui) -> AppResponse {
+ fn update(&mut self, ctx: &mut AppContext<'_>, egui_ctx: &egui::Context) {
if !self.initialized {
self.initialized = true;
- self.init(ui.ctx().clone(), ctx);
+ self.init(egui_ctx.clone(), ctx);
}
self.process_worker_msgs();
self.schedule_refresh();
+ }
+ fn render(&mut self, ctx: &mut AppContext<'_>, ui: &mut egui::Ui) -> AppResponse {
self.show(ui, ctx);
-
AppResponse::none()
}
}
diff --git a/crates/notedeck_dave/src/lib.rs b/crates/notedeck_dave/src/lib.rs
@@ -2156,7 +2156,7 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
}
/// Handle a keybinding action
- fn handle_key_action(&mut self, key_action: KeyAction, ui: &egui::Ui) {
+ fn handle_key_action(&mut self, key_action: KeyAction, egui_ctx: &egui::Context) {
let bt = self
.session_manager
.get_active()
@@ -2171,13 +2171,13 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
self.show_scene,
self.auto_steal_focus,
&mut self.home_session,
- ui.ctx(),
+ egui_ctx,
) {
KeyActionResult::ToggleView => {
self.show_scene = !self.show_scene;
}
KeyActionResult::HandleInterrupt => {
- self.handle_interrupt_request(ui.ctx());
+ self.handle_interrupt_request(egui_ctx);
}
KeyActionResult::CloneAgent => {
self.clone_active_agent();
@@ -2523,11 +2523,11 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
///
/// Collects negentropy protocol events from the relay, re-subscribes on
/// reconnect, and drives multi-round sync to fetch missing PNS events.
- fn process_negentropy_sync(&mut self, ctx: &mut AppContext<'_>, ui: &egui::Ui) {
+ fn process_negentropy_sync(&mut self, ctx: &mut AppContext<'_>, egui_ctx: &egui::Context) {
let pns_sub_id = self.pns_relay_sub.clone();
let pns_relay = self.pns_relay_url.clone();
let mut neg_events: Vec<enostr::negentropy::NegEvent> = Vec::new();
- try_process_events_core(ctx, &mut self.pool, ui.ctx(), |app_ctx, pool, ev| {
+ try_process_events_core(ctx, &mut self.pool, egui_ctx, |app_ctx, pool, ev| {
if ev.relay == pns_relay {
if let enostr::RelayEvent::Opened = (&ev.event).into() {
neg_events.push(enostr::negentropy::NegEvent::RelayOpened);
@@ -2608,7 +2608,7 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
///
/// Restores sessions from ndb, triggers initial negentropy sync,
/// and sets up relay subscriptions.
- fn initialize_once(&mut self, ctx: &mut AppContext<'_>, ui: &egui::Ui) {
+ fn initialize_once(&mut self, ctx: &mut AppContext<'_>, egui_ctx: &egui::Context) {
self.sessions_restored = true;
self.restore_sessions_from_ndb(ctx);
@@ -2624,7 +2624,7 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
let pns_keys = enostr::pns::derive_pns_keys(&sk.secret_bytes());
// Ensure the PNS relay is in the pool
- let egui_ctx = ui.ctx().clone();
+ let egui_ctx = egui_ctx.clone();
let wakeup = move || egui_ctx.request_repaint();
if let Err(e) = self.pool.add_url(self.pns_relay_url.clone(), wakeup) {
tracing::warn!("failed to add PNS relay {}: {:?}", self.pns_relay_url, e);
@@ -2674,10 +2674,8 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
}
impl notedeck::App for Dave {
- fn update(&mut self, ctx: &mut AppContext<'_>, ui: &mut egui::Ui) -> AppResponse {
- let mut app_action: Option<AppAction> = None;
-
- self.process_negentropy_sync(ctx, ui);
+ fn update(&mut self, ctx: &mut AppContext<'_>, egui_ctx: &egui::Context) {
+ self.process_negentropy_sync(ctx, egui_ctx);
// Poll for external spawn-agent commands via IPC
self.poll_ipc_commands();
@@ -2686,13 +2684,13 @@ impl notedeck::App for Dave {
let pending = std::mem::take(&mut self.pending_summaries);
for note_id in pending {
if let Some(sid) = self.build_summary_session(ctx.ndb, ¬e_id) {
- self.send_user_message_for(sid, ctx, ui.ctx());
+ self.send_user_message_for(sid, ctx, egui_ctx);
}
}
// One-time initialization on first update
if !self.sessions_restored {
- self.initialize_once(ctx, ui);
+ self.initialize_once(ctx, egui_ctx);
}
// Poll for external editor completion
@@ -2719,7 +2717,7 @@ impl notedeck::App for Dave {
.get(sid)
.is_some_and(|s| s.should_dispatch_remote_message());
if should_dispatch {
- self.send_user_message_for(sid, ctx, ui.ctx());
+ self.send_user_message_for(sid, ctx, egui_ctx);
}
}
@@ -2741,13 +2739,13 @@ impl notedeck::App for Dave {
.map(|s| s.ai_mode)
.unwrap_or(self.ai_mode);
if let Some(key_action) = check_keybindings(
- ui.ctx(),
+ egui_ctx,
has_pending_permission,
has_pending_question,
in_tentative_state,
active_ai_mode,
) {
- self.handle_key_action(key_action, ui);
+ self.handle_key_action(key_action, egui_ctx);
}
// Check if interrupt confirmation has timed out
@@ -2801,7 +2799,7 @@ impl notedeck::App for Dave {
get_backend(&self.backends, bt).set_permission_mode(
backend_sid,
mode,
- ui.ctx().clone(),
+ egui_ctx.clone(),
);
}
@@ -2854,14 +2852,7 @@ impl notedeck::App for Dave {
// Raise the OS window when auto-steal switches to a NeedsInput session
if stole_focus {
- activate_app(ui.ctx());
- }
- }
-
- // Render UI and handle actions
- if let Some(action) = self.ui(ctx, ui).action {
- if let Some(returned_action) = self.handle_ui_action(action, ctx, ui) {
- app_action = Some(returned_action);
+ activate_app(egui_ctx);
}
}
@@ -2871,7 +2862,17 @@ impl notedeck::App for Dave {
"Session {}: dispatching queued message via send_user_message_for",
session_id
);
- self.send_user_message_for(session_id, ctx, ui.ctx());
+ self.send_user_message_for(session_id, ctx, egui_ctx);
+ }
+ }
+
+ fn render(&mut self, ctx: &mut AppContext<'_>, ui: &mut egui::Ui) -> AppResponse {
+ let mut app_action: Option<AppAction> = None;
+
+ if let Some(action) = self.ui(ctx, ui).action {
+ if let Some(returned_action) = self.handle_ui_action(action, ctx, ui) {
+ app_action = Some(returned_action);
+ }
}
AppResponse::action(app_action)
diff --git a/crates/notedeck_messages/src/lib.rs b/crates/notedeck_messages/src/lib.rs
@@ -55,13 +55,12 @@ impl Default for MessagesApp {
impl App for MessagesApp {
#[profiling::function]
- fn update(&mut self, ctx: &mut AppContext<'_>, ui: &mut egui::Ui) -> AppResponse {
+ fn update(&mut self, ctx: &mut AppContext<'_>, egui_ctx: &egui::Context) {
let Some(cache) = self.messages.get_current_mut(ctx.accounts) else {
- login_nsec_prompt(ui, ctx.i18n);
- return AppResponse::none();
+ return;
};
- self.loader.start(ui.ctx().clone(), ctx.ndb.clone());
+ self.loader.start(egui_ctx.clone(), ctx.ndb.clone());
's: {
let Some(secret) = &ctx.accounts.get_selected_account().key.secret_key else {
@@ -93,7 +92,7 @@ impl App for MessagesApp {
match cache.state {
ConversationListState::Initializing => {
- initialize(ctx, cache, is_narrow(ui.ctx()), &self.loader);
+ initialize(ctx, cache, is_narrow(egui_ctx), &self.loader);
}
ConversationListState::Loading { subscription } => {
if let Some(sub) = subscription {
@@ -113,8 +112,16 @@ impl App for MessagesApp {
cache,
&self.loader,
&mut self.inflight_messages,
- is_narrow(ui.ctx()),
+ is_narrow(egui_ctx),
);
+ }
+
+ #[profiling::function]
+ fn render(&mut self, ctx: &mut AppContext<'_>, ui: &mut egui::Ui) -> AppResponse {
+ let Some(cache) = self.messages.get_current_mut(ctx.accounts) else {
+ login_nsec_prompt(ui, ctx.i18n);
+ return AppResponse::none();
+ };
let selected_pubkey = ctx.accounts.selected_account_pubkey();
diff --git a/crates/notedeck_nostrverse/src/lib.rs b/crates/notedeck_nostrverse/src/lib.rs
@@ -645,23 +645,15 @@ impl NostrverseApp {
}
impl notedeck::App for NostrverseApp {
- fn update(&mut self, ctx: &mut AppContext<'_>, ui: &mut egui::Ui) -> AppResponse {
- // Initialize on first frame
+ fn update(&mut self, ctx: &mut AppContext<'_>, _egui_ctx: &egui::Context) {
self.initialize(ctx);
-
- // Poll for space event updates
self.poll_space_updates(ctx.ndb);
-
- // Poll for completed model downloads
self.poll_model_downloads();
-
- // Presence: publish, poll, expire
self.tick_presence(ctx);
-
- // Sync state to 3D scene
self.sync_scene();
+ }
- // Get available size before layout
+ fn render(&mut self, ctx: &mut AppContext<'_>, ui: &mut egui::Ui) -> AppResponse {
let available = ui.available_size();
let panel_width = 240.0;
diff --git a/crates/notedeck_notebook/src/lib.rs b/crates/notedeck_notebook/src/lib.rs
@@ -28,9 +28,7 @@ impl Default for Notebook {
}
impl notedeck::App for Notebook {
- fn update(&mut self, _ctx: &mut AppContext<'_>, ui: &mut egui::Ui) -> AppResponse {
- //let app_action: Option<AppAction> = None;
-
+ fn render(&mut self, _ctx: &mut AppContext<'_>, ui: &mut egui::Ui) -> AppResponse {
if !self.loaded {
self.scene_rect = ui.available_rect_before_wrap();
self.loaded = true;
diff --git a/crates/notedeck_wasm/src/lib.rs b/crates/notedeck_wasm/src/lib.rs
@@ -98,7 +98,7 @@ impl WasmApp {
}
impl notedeck::App for WasmApp {
- fn update(&mut self, _ctx: &mut AppContext<'_>, ui: &mut egui::Ui) -> AppResponse {
+ fn render(&mut self, _ctx: &mut AppContext<'_>, ui: &mut egui::Ui) -> AppResponse {
let cmds = self.run_wasm_frame(ui.available_size());
let new_events = commands::render_commands(&cmds, ui);
self.env.as_mut(&mut self.store).button_events = new_events;