// SPDX-License-Identifier: LGPL-2.1-only // Copyright (C) 2026 Liberux Labs, S. L. use smithay_client_toolkit:: { compositor::CompositorHandler, delegate_compositor, delegate_subcompositor, delegate_foreign_toplevel_list, delegate_layer, delegate_output, delegate_registry, delegate_seat, delegate_keyboard, delegate_pointer, delegate_touch, delegate_shm, delegate_xdg_popup, delegate_xdg_shell, delegate_xdg_window, delegate_session_lock, foreign_toplevel_list::{ ForeignToplevelList, ForeignToplevelListHandler }, output::{ OutputHandler, OutputState }, registry::{ ProvidesRegistryState, RegistryState }, registry_handlers, seat::{ Capability, SeatHandler, SeatState }, shell:: { WaylandSurface, wlr_layer::{ LayerShellHandler, LayerSurface, LayerSurfaceConfigure }, xdg::XdgSurface, xdg::popup::{ Popup, PopupConfigure, PopupHandler }, xdg::window::{ Window, WindowConfigure, WindowHandler }, }, shm::{ Shm, ShmHandler }, session_lock::{ SessionLock, SessionLockHandler, SessionLockSurface, SessionLockSurfaceConfigure }, }; use smithay_client_toolkit::reexports::protocols::ext::foreign_toplevel_list::v1::client::ext_foreign_toplevel_handle_v1::ExtForeignToplevelHandleV1; use smithay_client_toolkit::reexports::client:: { protocol:: { wl_callback::{ self, WlCallback }, wl_output::{ self, WlOutput }, wl_surface::WlSurface, wl_seat::WlSeat, }, Connection, Dispatch, Proxy, QueueHandle, }; use wayland_protocols::wp::text_input::zv3::client:: { zwp_text_input_manager_v3::ZwpTextInputManagerV3, zwp_text_input_v3::{ self, ZwpTextInputV3 }, }; use crate::app::{ App, ToplevelEvent }; use super::app_data::AppData; impl CompositorHandler for AppData { fn scale_factor_changed( &mut self, _: &Connection, _: &QueueHandle, surface: &WlSurface, new_factor: i32 ) { if new_factor <= 0 { return; } let Some( focus ) = self.focus_for_surface( surface ) else { return }; let ( pw, ph ) = { let shm = &self.shm; let ss = match focus { super::SurfaceFocus::Main => &mut self.main, super::SurfaceFocus::Overlay( id ) => match self.overlays.get_mut( &id ) { Some( s ) => s, None => return, }, }; if new_factor == ss.scale_factor { return; } ss.scale_factor = new_factor; surface.set_buffer_scale( new_factor ); if let Some( ref mut canvas ) = ss.canvas { canvas.set_dpi_scale( new_factor as f32 ); } let pw = ss.width * new_factor as u32; let ph = ss.height * new_factor as u32; // Resize whichever rendering target is active. The two are mutually // exclusive so only one branch runs. if let Some( ref es ) = ss.egl_surface { es.resize( pw as i32, ph as i32 ); // Canvas FBO reallocation is deferred to the draw path: it needs // `eglMakeCurrent` first. } else { ss.pool = Some( smithay_client_toolkit::shm::slot::SlotPool::new( ( pw * ph * 4 ) as usize, shm, ).expect( "pool" ), ); if let Some( ref mut canvas ) = ss.canvas { canvas.resize( pw, ph ); } } ss.request_redraw(); ( pw, ph ) }; // Notify the app of the new physical dimensions. The previous // `on_resize` it received was scaled with the OLD factor, so any // app-side state keyed off those pixels is now stale. Only fire // for the main surface — overlay resizes don't go through // `App::on_resize`. if matches!( focus, super::SurfaceFocus::Main ) { self.app.on_resize( pw, ph ); self.dirty_caches(); } } fn transform_changed( &mut self, _: &Connection, _: &QueueHandle, _: &WlSurface, _: wl_output::Transform ) {} fn frame( &mut self, _conn: &Connection, _qh: &QueueHandle, _surface: &WlSurface, _time: u32, ) { // Do NOT set needs_redraw here — that would create an infinite 60 fps loop // (commit → frame callback → redraw → commit → ...). Redraws are driven // exclusively by input events, poll_external messages, and configure events. } fn surface_enter( &mut self, _: &Connection, _: &QueueHandle, _: &WlSurface, _: &WlOutput ) {} fn surface_leave( &mut self, _: &Connection, _: &QueueHandle, _: &WlSurface, _: &WlOutput ) {} } impl LayerShellHandler for AppData { fn closed( &mut self, _conn: &Connection, _qh: &QueueHandle, layer: &LayerSurface, ) { match self.focus_for_surface( layer.wl_surface() ) { Some( super::SurfaceFocus::Main ) | None => { if self.app.on_close_requested() { self.exit_requested = true; } } Some( super::SurfaceFocus::Overlay( id ) ) => { // Compositor asked us to destroy this overlay. Drop it // synchronously *and* rewrite every per-device focus that // pointed at it — otherwise the next event in this same // dispatch (touch up, IME done, pointer leave) lands on a // freed surface and `surface()` / `surface_mut()` panic. // `reconcile_overlays` will still run afterwards and skip // re-creating the entry as long as the app stops returning // its id from `overlays()`. self.discard_overlay( id ); } } } fn configure( &mut self, _conn: &Connection, _qh: &QueueHandle, layer: &LayerSurface, configure: LayerSurfaceConfigure, _serial: u32, ) { let ( w, h ) = configure.new_size; let ( w, h ) = ( w.max( 1 ), h.max( 1 ) ); match self.focus_for_surface( layer.wl_surface() ) { Some( super::SurfaceFocus::Main ) | None => { self.on_configure( w, h ); } Some( super::SurfaceFocus::Overlay( id ) ) => { if let Some( ss ) = self.overlays.get_mut( &id ) { ss.on_configure( &self.shm, self.egl_context.as_ref(), w, h ); } } } } } impl WindowHandler for AppData { fn request_close( &mut self, _conn: &Connection, _qh: &QueueHandle, _window: &Window, ) { if self.app.on_close_requested() { self.exit_requested = true; } } fn configure( &mut self, _conn: &Connection, _qh: &QueueHandle, window: &Window, configure: WindowConfigure, _serial: u32, ) { // Mutter ignores set_fullscreen sent before the surface is // mapped, so reapply on the first configure. if self.pending_fullscreen { window.set_fullscreen( None ); self.pending_fullscreen = false; } if self.pending_size_hint_unpin { window.set_max_size( None ); self.pending_size_hint_unpin = false; } let ( hint_w, hint_h ) = self.app.window_size_hint().unwrap_or( ( 800, 600 ) ); let w = configure.new_size.0.map( |v| v.get() ).unwrap_or( hint_w ); let h = configure.new_size.1.map( |v| v.get() ).unwrap_or( hint_h ); window.xdg_surface().set_window_geometry( 0, 0, w as i32, h as i32 ); self.on_configure( w, h ); } } impl ShmHandler for AppData { fn shm_state( &mut self ) -> &mut Shm { &mut self.shm } } impl OutputHandler for AppData { fn output_state( &mut self ) -> &mut OutputState { &mut self.output_state } fn new_output( &mut self, _: &Connection, qh: &QueueHandle, output: WlOutput ) { // If we were waiting for an output to assign the layer surface to, create it now. // Same for any overlays that were created before an output existed. if let Some( ref layer_shell ) = self.layer_shell { self.main.surface.materialize( &self.compositor_state, layer_shell, qh, &output ); for ss in self.overlays.values_mut() { ss.surface.materialize( &self.compositor_state, layer_shell, qh, &output ); } } } fn update_output( &mut self, _: &Connection, _: &QueueHandle, _: WlOutput ) {} fn output_destroyed( &mut self, _: &Connection, _: &QueueHandle, _: WlOutput ) {} } impl SeatHandler for AppData { fn seat_state( &mut self ) -> &mut SeatState { &mut self.seat_state } fn new_seat( &mut self, _conn: &Connection, qh: &QueueHandle, seat: WlSeat, ) { // First seat → first data device. We only need one device // regardless of how many seats appear (the typical desktop // session has exactly one); additional seats reuse the same // device-bound clipboard semantics by construction because // `wl_data_device_manager.get_data_device` takes the seat as // argument and the compositor manages routing. if self.data_device.is_none() { if let Some( ref ddm ) = self.data_device_manager { self.data_device = Some( ddm.get_data_device( qh, &seat ) ); } } } fn new_capability( &mut self, _conn: &Connection, qh: &QueueHandle, seat: WlSeat, capability: Capability, ) { match capability { Capability::Keyboard if self.keyboard.is_none() => { self.keyboard = Some( self.seat_state .get_keyboard( qh, &seat, None ) .expect( "keyboard" ), ); } Capability::Pointer if self.pointer.is_none() => { let pointer = self.seat_state .get_pointer( qh, &seat ) .expect( "pointer" ); // Create a per-pointer cursor-shape device when the // compositor advertises wp_cursor_shape_v1. The device // outlives the WlPointer we just created and is what // `set_shape(serial, shape)` is called on. if let Some( ref mgr ) = self.cursor_shape_manager { self.cursor_shape_device = Some( mgr.get_shape_device( &pointer, qh ) ); } self.pointer = Some( pointer ); } Capability::Touch if self.touch.is_none() => { self.touch = Some( self.seat_state .get_touch( qh, &seat ) .expect( "touch" ), ); } _ => {} } } fn remove_capability( &mut self, _conn: &Connection, _qh: &QueueHandle, _seat: WlSeat, capability: Capability, ) { match capability { Capability::Keyboard => { self.keyboard = None; } Capability::Pointer => { self.pointer = None; } Capability::Touch => { self.touch = None; } _ => {} } } fn remove_seat( &mut self, _conn: &Connection, _qh: &QueueHandle, _seat: WlSeat, ) {} } impl PopupHandler for AppData { fn configure( &mut self, _conn: &Connection, _qh: &QueueHandle, popup: &Popup, config: PopupConfigure, ) { let ( w, h ) = ( config.width.max( 1 ) as u32, config.height.max( 1 ) as u32 ); if let Some( super::SurfaceFocus::Overlay( id ) ) = self.focus_for_surface( popup.wl_surface() ) { if let Some( ss ) = self.overlays.get_mut( &id ) { ss.on_configure( &self.shm, self.egl_context.as_ref(), w, h ); } } } fn done( &mut self, _conn: &Connection, _qh: &QueueHandle, popup: &Popup, ) { if let Some( super::SurfaceFocus::Overlay( id ) ) = self.focus_for_surface( popup.wl_surface() ) { if let Some( msg ) = self.overlay_dismiss_msg( id ) { self.pending_msgs.push( msg ); } // Synchronous teardown plus per-device focus cleanup. See // `discard_overlay` for the panic case this prevents. self.discard_overlay( id ); } } } // --- Delegate macros --- impl SessionLockHandler for AppData { fn locked( &mut self, _conn: &Connection, qh: &QueueHandle, session_lock: SessionLock ) { if let Some( output ) = self.output_state.outputs().next() { let surface = self.compositor_state.create_surface( qh ); let lock_surface = session_lock.create_lock_surface( surface, &output, qh ); self.main.surface = super::SurfaceKind::Lock( lock_surface ); } self.session_lock = Some( session_lock ); } fn finished( &mut self, _conn: &Connection, _qh: &QueueHandle, _session_lock: SessionLock ) { // Compositor refused the lock or ended it; nothing left to show. self.exit_requested = true; } fn configure( &mut self, _conn: &Connection, _qh: &QueueHandle, _surface: SessionLockSurface, configure: SessionLockSurfaceConfigure, _serial: u32, ) { let ( w, h ) = configure.new_size; self.on_configure( w.max( 1 ), h.max( 1 ) ); } } delegate_compositor!( @ AppData ); delegate_subcompositor!( @ AppData ); delegate_output!( @ AppData ); delegate_shm!( @ AppData ); delegate_seat!( @ AppData ); delegate_keyboard!( @ AppData ); delegate_pointer!( @ AppData ); delegate_touch!( @ AppData ); delegate_layer!( @ AppData ); delegate_session_lock!( @ AppData ); delegate_xdg_shell!( @ AppData ); delegate_xdg_window!( @ AppData ); delegate_xdg_popup!( @ AppData ); delegate_foreign_toplevel_list!( @ AppData ); delegate_registry!( @ AppData ); smithay_client_toolkit::delegate_activation!( @ AppData ); smithay_client_toolkit::delegate_data_device!( @ AppData ); // --- `xdg-activation-v1` handler --- // // We only honour inbound activation here: when a token issued for our // own request gets delivered, we activate the main surface. Outbound // requests (so the app can pass a token to another app) are out of // scope for now — adding them only requires a new public method on // `AppData` and an extra trait method on `App`. impl smithay_client_toolkit::activation::ActivationHandler for AppData { type RequestData = smithay_client_toolkit::activation::RequestData; fn new_token( &mut self, token: String, _data: &Self::RequestData ) { if let ( Some( ref activation ), Some( wl ) ) = ( self.activation_state.as_ref(), self.main.surface.try_wl_surface() ) { activation.activate::( &wl, token ); } } } impl ProvidesRegistryState for AppData { fn registry( &mut self ) -> &mut RegistryState { &mut self.registry_state } registry_handlers![ OutputState, SeatState ]; } // --- `ext-foreign-toplevel-list-v1` handler --- // // SCTK does the wire-level dispatch (open / close / done / app_id / // title / identifier events arrive on the inner `ForeignToplevelList` // helper); we surface the high-level open / update / close callbacks // to apps through [`App::on_toplevel_event`]. Returned messages join // `pending_msgs` and flow through the regular `update` cycle, so the // invalidation pipeline (`App::invalidate_after`) decides which // surfaces to redraw — apps that only need the dock to refresh can // scope to `SurfaceTarget::Main` and skip overlays. // // The `id` field reported to apps is the Wayland protocol id of the // handle proxy — unique per session and stable for the handle's // lifetime, the same value paired across `Opened` and the matching // `Closed`. /// Cascade for the per-toplevel display string crustace-style shells use to /// register a window: prefer `app_id` (sets `.desktop` matching), fall back /// to `title` (visible to the user), and finally to the protocol-issued /// `identifier` (always present, unique per handle). Without the cascade, /// any client that never set `app_id` (e.g. winit-windowed compositors /// before they bind their xdg surface) was silently invisible to the dock. fn toplevel_display_id( list: &ForeignToplevelList, 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(); }; info.app_id } impl ForeignToplevelListHandler for AppData { fn foreign_toplevel_list_state( &mut self ) -> &mut ForeignToplevelList { &mut self.foreign_toplevel_list } fn new_toplevel( &mut self, _conn: &Connection, _qh: &QueueHandle, handle: ExtForeignToplevelHandleV1, ) { let id = handle.id().protocol_id(); let app_id = toplevel_display_id( &self.foreign_toplevel_list, &handle ); if let Some( msg ) = self.app.on_toplevel_event( ToplevelEvent::Opened { id, app_id } ) { self.pending_msgs.push( msg ); } } fn update_toplevel( &mut self, _conn: &Connection, _qh: &QueueHandle, handle: ExtForeignToplevelHandleV1, ) { // Re-emit `Opened` with the latest info. Compositors usually // only fire this on title changes; the `app_id` is committed // once on the initial `done` and never replaced. Apps whose // `RunningApps`-style bookkeeping is keyed by `(id, app_id)` // can treat repeated opens as idempotent and pay nothing here, // but the app_id-can-change-mid-life case still produces a // correct sequence (old refcount drops, new one bumps). let id = handle.id().protocol_id(); let app_id = toplevel_display_id( &self.foreign_toplevel_list, &handle ); if let Some( msg ) = self.app.on_toplevel_event( ToplevelEvent::Opened { id, app_id } ) { self.pending_msgs.push( msg ); } } fn toplevel_closed( &mut self, _conn: &Connection, _qh: &QueueHandle, handle: ExtForeignToplevelHandleV1, ) { let id = handle.id().protocol_id(); if let Some( msg ) = self.app.on_toplevel_event( ToplevelEvent::Closed { id } ) { self.pending_msgs.push( msg ); } } } // --- Dispatch impls for zwp_text_input_v3 --- impl Dispatch for AppData { fn event( _state: &mut Self, _proxy: &ZwpTextInputManagerV3, _event: ::Event, _data: &(), _conn: &Connection, _qh: &QueueHandle, ) { // no events from manager } } // Frame-callback routing. // // Each `draw_*` path requests a `wl_surface.frame` with `SurfaceFocus` user- // data identifying which `SurfaceState` it belongs to. The compositor fires // the callback when the surface is ready for its next commit (display refresh, // VRR cadence, "screen is on", …). Clearing `frame_pending` unblocks the run // loop so the next iteration is allowed to draw that surface again. // // While the app is animating we re-arm the surface for redraw inline so the // loop keeps ticking at the compositor's pace without needing a fixed-period // timer. impl Dispatch for AppData { fn event( state: &mut Self, _proxy: &WlCallback, _event: wl_callback::Event, focus: &super::SurfaceFocus, _conn: &Connection, _qh: &QueueHandle, ) { let is_animating = state.app.is_animating(); match *focus { super::SurfaceFocus::Main => { state.main.frame_pending = false; if is_animating { if state.app.subsurface_motion_only() { // The main buffer is unchanged; only the subsurface // moves (repositioned by the per-frame reconcile). // Keep the vsync cadence with a bare frame callback + // commit — no buffer attach, no re-raster. if let Some( wl ) = state.main.surface.try_wl_surface().cloned() { let _ = wl.frame( &state.qh, super::SurfaceFocus::Main ); wl.commit(); state.main.frame_pending = true; } } else { state.view_dirty = true; state.main.request_redraw(); } } } super::SurfaceFocus::Overlay( id ) => { if let Some( ss ) = state.overlays.get_mut( &id ) { ss.frame_pending = false; if is_animating { state.overlays_dirty = true; ss.request_redraw(); } } } } } } impl Dispatch for AppData { fn event( state: &mut Self, _proxy: &ZwpTextInputV3, event: zwp_text_input_v3::Event, _data: &(), _conn: &Connection, _qh: &QueueHandle, ) { let focus = state.keyboard_focus; match event { zwp_text_input_v3::Event::CommitString { text } => { if let Some( text ) = text { if !text.is_empty() { state.handle_text_insert( focus, &text ); } } } zwp_text_input_v3::Event::DeleteSurroundingText { before_length, after_length } => { state.handle_delete_surrounding( focus, before_length, after_length ); } zwp_text_input_v3::Event::Done { .. } => { if let Some( ss ) = state.try_surface_mut( focus ) { ss.request_redraw(); } } zwp_text_input_v3::Event::Enter { .. } => { state.reenable_text_input(); } _ => {} } } }