app, event_loop: first-frame-committed hook and foreign_toplevel app_id only
Some checks failed
CI / build + test (push) Has been cancelled
CI / cargo audit (push) Has been cancelled

Two independent changes, both blockers for a working desktop session through the loginmanager-daemon handoff and the dock's running-app icons.
`App::on_first_frame_committed` is a new trait hook fired exactly once, immediately after the very first `wl_surface.commit` of a rendered buffer on the main surface. `AppData` grows a `first_frame_committed: bool`, `draw_frame` now returns whether this call performed that first commit, and `try_run` invokes the hook after the borrows held during the draw are released. Used by the loginmanager-daemon-aware crustace path to signal "ready to be presented" back to the daemon as soon as the GPU has the first frame — the actual present can still be deferred under VT switching (no DRM master yet), but the client-side commit is the right edge for handoff.
`toplevel_display_id` in the foreign-toplevel-list handler no longer falls back to `info.title` or `info.identifier` when `info.app_id` is empty. Smithay creates each `ext-foreign-toplevel-list-v1` handle with `app_id = ""` and `init_new_instance` flushes a `done` immediately, so subscribers used to see the protocol-level identifier (a 32-char `Alphanumeric` random token) as the "app id" of every new toplevel — and remained stuck on it whenever the client's real `set_app_id` arrived between done events but the subscribing app's matcher couldn't resolve it to a `.desktop` entry. Returning the raw `app_id` (empty or not) makes that first transient `done` ignorable by the consumer's own empty-string guard; the second `done`, carrying the real app id, is processed normally.
This commit is contained in:
2026-05-21 01:51:19 +02:00
parent 78a7ae151c
commit 0e52274053
5 changed files with 38 additions and 5 deletions

View File

@@ -293,6 +293,14 @@ pub trait App: 'static
/// Return any pending messages from external sources (timers, async, etc.). /// Return any pending messages from external sources (timers, async, etc.).
fn poll_external( &mut self ) -> Vec<Self::Message> { vec![] } fn poll_external( &mut self ) -> Vec<Self::Message> { 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, /// Called when the surface is asked to close (compositor request, titlebar button,
/// layer-shell closed event). Return `true` to allow the application to exit, /// layer-shell closed event). Return `true` to allow the application to exit,
/// or `false` to cancel. /// or `false` to cancel.

View File

@@ -232,6 +232,9 @@ pub struct AppData<A: App>
pub view_dirty: bool, pub view_dirty: bool,
/// Counterpart of `view_dirty` for `cached_overlays`. /// Counterpart of `view_dirty` for `cached_overlays`.
pub overlays_dirty: bool, 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<A: App> AppData<A> impl<A: App> AppData<A>

View File

@@ -31,7 +31,10 @@ pub( crate ) fn pick_shm_format( shm: &Shm ) -> ( wl_shm::Format, bool )
} }
} }
pub( crate ) fn draw_frame<A: App>( data: &mut AppData<A> ) /// 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<A: App>( data: &mut AppData<A> ) -> bool
{ {
let main_view = data.cached_view.as_ref().expect( "view cache populated" ); let main_view = data.cached_view.as_ref().expect( "view cache populated" );
let overlays = data.cached_overlays.as_ref().expect( "overlays cache populated" ); let overlays = data.cached_overlays.as_ref().expect( "overlays cache populated" );
@@ -43,6 +46,7 @@ pub( crate ) fn draw_frame<A: App>( data: &mut AppData<A> )
let egl_ctx = data.egl_context.as_ref(); let egl_ctx = data.egl_context.as_ref();
let qh = &data.qh; let qh = &data.qh;
let mut first_commit_now = false;
if data.main.configured && data.main.needs_redraw && !data.main.frame_pending if data.main.configured && data.main.needs_redraw && !data.main.frame_pending
{ {
let req_frame = | wl: &WlSurface | { let _ = wl.frame( qh, SurfaceFocus::Main ); }; let req_frame = | wl: &WlSurface | { let _ = wl.frame( qh, SurfaceFocus::Main ); };
@@ -60,6 +64,11 @@ pub( crate ) fn draw_frame<A: App>( data: &mut AppData<A> )
); );
data.main.needs_redraw = false; data.main.needs_redraw = false;
data.main.last_draw = std::time::Instant::now(); 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 for spec in overlays
@@ -86,6 +95,8 @@ pub( crate ) fn draw_frame<A: App>( data: &mut AppData<A> )
ss.last_draw = std::time::Instant::now(); ss.last_draw = std::time::Instant::now();
} }
} }
first_commit_now
} }
fn draw_surface<Msg: Clone>( fn draw_surface<Msg: Clone>(

View File

@@ -468,10 +468,16 @@ fn toplevel_display_id(
handle: &ExtForeignToplevelHandleV1, handle: &ExtForeignToplevelHandleV1,
) -> String ) -> 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(); }; let Some( info ) = list.info( handle ) else { return String::new(); };
if !info.app_id.is_empty() { return info.app_id; } info.app_id
if !info.title.is_empty() { return info.title; }
info.identifier
} }
impl<A: App> ForeignToplevelListHandler for AppData<A> impl<A: App> ForeignToplevelListHandler for AppData<A>

View File

@@ -300,6 +300,7 @@ pub( crate ) fn try_run<A: App>( app: A ) -> Result<(), RunError>
cached_overlays: None, cached_overlays: None,
view_dirty: true, view_dirty: true,
overlays_dirty: true, overlays_dirty: true,
first_frame_committed: false,
}; };
// Register a calloop channel so the app can send messages from any thread. // Register a calloop channel so the app can send messages from any thread.
@@ -525,7 +526,11 @@ pub( crate ) fn try_run<A: App>( app: A ) -> Result<(), RunError>
data.cached_overlays = Some( specs ); data.cached_overlays = Some( specs );
data.overlays_dirty = false; 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 // Push the freshly-laid-out widget tree to AccessKit. The
// closure only runs when an AT client is actually // closure only runs when an AT client is actually