diff --git a/src/app.rs b/src/app.rs index 32200fd..af58467 100644 --- a/src/app.rs +++ b/src/app.rs @@ -293,6 +293,14 @@ pub trait App: 'static /// Return any pending messages from external sources (timers, async, etc.). fn poll_external( &mut self ) -> Vec { vec![] } + /// Called once, immediately after the first buffer for the main surface + /// has been rendered and committed to the compositor. Note: this is the + /// client-side commit, not the compositor's present — under VT switching + /// the actual present can be deferred (no DRM master yet), so a handoff + /// signal that fires here is enough to tell a parent "ready to be + /// presented as soon as my VT becomes active". + fn on_first_frame_committed( &mut self ) {} + /// Called when the surface is asked to close (compositor request, titlebar button, /// layer-shell closed event). Return `true` to allow the application to exit, /// or `false` to cancel. diff --git a/src/event_loop/app_data.rs b/src/event_loop/app_data.rs index 88bf2e4..61a7cf7 100644 --- a/src/event_loop/app_data.rs +++ b/src/event_loop/app_data.rs @@ -232,6 +232,9 @@ pub struct AppData pub view_dirty: bool, /// Counterpart of `view_dirty` for `cached_overlays`. pub overlays_dirty: bool, + /// Set after the first commit of a rendered buffer on the main surface. + /// Used to drive `App::on_first_frame_committed` exactly once. + pub first_frame_committed: bool, } impl AppData diff --git a/src/event_loop/frame.rs b/src/event_loop/frame.rs index c270579..eae5612 100644 --- a/src/event_loop/frame.rs +++ b/src/event_loop/frame.rs @@ -31,7 +31,10 @@ pub( crate ) fn pick_shm_format( shm: &Shm ) -> ( wl_shm::Format, bool ) } } -pub( crate ) fn draw_frame( data: &mut AppData ) +/// Returns `true` when this call performed the very first commit on the main +/// surface — the run loop uses that to fire `App::on_first_frame_committed` +/// exactly once, after the borrows held by draw_frame have been released. +pub( crate ) fn draw_frame( data: &mut AppData ) -> bool { let main_view = data.cached_view.as_ref().expect( "view cache populated" ); let overlays = data.cached_overlays.as_ref().expect( "overlays cache populated" ); @@ -43,6 +46,7 @@ pub( crate ) fn draw_frame( data: &mut AppData ) let egl_ctx = data.egl_context.as_ref(); let qh = &data.qh; + let mut first_commit_now = false; if data.main.configured && data.main.needs_redraw && !data.main.frame_pending { let req_frame = | wl: &WlSurface | { let _ = wl.frame( qh, SurfaceFocus::Main ); }; @@ -60,6 +64,11 @@ pub( crate ) fn draw_frame( data: &mut AppData ) ); data.main.needs_redraw = false; data.main.last_draw = std::time::Instant::now(); + if !data.first_frame_committed + { + data.first_frame_committed = true; + first_commit_now = true; + } } for spec in overlays @@ -86,6 +95,8 @@ pub( crate ) fn draw_frame( data: &mut AppData ) ss.last_draw = std::time::Instant::now(); } } + + first_commit_now } fn draw_surface( diff --git a/src/event_loop/handlers.rs b/src/event_loop/handlers.rs index 57cbc65..04f9848 100644 --- a/src/event_loop/handlers.rs +++ b/src/event_loop/handlers.rs @@ -468,10 +468,16 @@ fn toplevel_display_id( handle: &ExtForeignToplevelHandleV1, ) -> String { + // Only ever surface the client's `app_id`. The protocol-level + // `identifier` is a server-internal random opaque token, not an + // app handle; smithay creates the toplevel handle with an empty + // `app_id` and `init_new_instance` flushes a `done` immediately, + // so apps subscribing through this binding would otherwise see + // the random identifier as their "app id" until the client's + // real `set_app_id` lands. `title` is also unsuitable — it + // changes every time the window's title text updates. let Some( info ) = list.info( handle ) else { return String::new(); }; - if !info.app_id.is_empty() { return info.app_id; } - if !info.title.is_empty() { return info.title; } - info.identifier + info.app_id } impl ForeignToplevelListHandler for AppData diff --git a/src/event_loop/run.rs b/src/event_loop/run.rs index afc9551..d95c4ca 100644 --- a/src/event_loop/run.rs +++ b/src/event_loop/run.rs @@ -300,6 +300,7 @@ pub( crate ) fn try_run( app: A ) -> Result<(), RunError> cached_overlays: None, view_dirty: true, overlays_dirty: true, + first_frame_committed: false, }; // Register a calloop channel so the app can send messages from any thread. @@ -525,7 +526,11 @@ pub( crate ) fn try_run( app: A ) -> Result<(), RunError> data.cached_overlays = Some( specs ); data.overlays_dirty = false; } - draw_frame( &mut data ); + let first_commit_now = draw_frame( &mut data ); + if first_commit_now + { + data.app.on_first_frame_committed(); + } // Push the freshly-laid-out widget tree to AccessKit. The // closure only runs when an AT client is actually