From 99a47678158b49ac7d5b72552d8a9281a701538b Mon Sep 17 00:00:00 2001 From: "Pedro M. de Echanove Pasquin" Date: Fri, 14 Aug 2026 13:01:15 +0200 Subject: [PATCH] =?UTF-8?q?event=5Floop:=20cover=20every=20output=20with?= =?UTF-8?q?=20a=20session-lock=20surface,=20with=20the=20app's=20backdrop?= =?UTF-8?q?=20on=20the=20secondary=20ones=20ext-session-lock-v1=20obliges?= =?UTF-8?q?=20the=20compositor=20to=20blank=20any=20output=20that=20has=20?= =?UTF-8?q?no=20lock=20surface,=20and=20the=20runtime=20only=20ever=20crea?= =?UTF-8?q?ted=20one,=20on=20the=20first=20output=20=E2=80=94=20so=20on=20?= =?UTF-8?q?a=20multi-monitor=20session=20the=20extended=20screens=20went?= =?UTF-8?q?=20black=20the=20moment=20the=20lock=20engaged.=20The=20lock=20?= =?UTF-8?q?grant=20now=20creates=20one=20lock=20surface=20per=20advertised?= =?UTF-8?q?=20output:=20the=20first=20keeps=20hosting=20the=20app's=20main?= =?UTF-8?q?=20surface=20exactly=20as=20before,=20and=20each=20remaining=20?= =?UTF-8?q?output=20gets=20its=20own=20surface=20tracked=20in=20`lock=5Fex?= =?UTF-8?q?tras`,=20paired=20with=20its=20`WlOutput`=20so=20configure,=20s?= =?UTF-8?q?cale=20and=20teardown=20can=20be=20routed=20per=20surface.=20Wh?= =?UTF-8?q?at=20those=20extra=20surfaces=20show=20is=20up=20to=20the=20app?= =?UTF-8?q?:=20a=20new=20`App::lock=5Fsecondary=5Fview(=20width,=20height?= =?UTF-8?q?=20)`=20returns=20the=20content=20for=20a=20secondary=20output?= =?UTF-8?q?=20of=20that=20physical=20size,=20and=20the=20default=20`None`?= =?UTF-8?q?=20paints=20the=20surface=20with=20`background=5Fcolor()`.=20Th?= =?UTF-8?q?e=20draw=20pass=20renders=20them=20without=20requesting=20frame?= =?UTF-8?q?=20callbacks=20=E2=80=94=20their=20content=20is=20static=20betw?= =?UTF-8?q?een=20invalidations,=20and=20a=20callback=20that=20is=20never?= =?UTF-8?q?=20re-requested=20would=20leave=20`frame=5Fpending`=20stuck=20?= =?UTF-8?q?=E2=80=94=20and=20invalidations=20that=20touch=20the=20main=20s?= =?UTF-8?q?urface=20also=20mark=20the=20extras,=20so=20a=20backdrop=20that?= =?UTF-8?q?=20follows=20main-surface=20state=20(wallpaper=20swaps,=20theme?= =?UTF-8?q?=20changes)=20stays=20in=20sync.=20The=20per-surface=20plumbing?= =?UTF-8?q?=20that=20only=20ever=20knew=20about=20the=20main=20surface=20l?= =?UTF-8?q?earns=20to=20route:=20`SessionLockHandler::configure`=20matches?= =?UTF-8?q?=20the=20surface=20against=20main=20and=20the=20extras=20instea?= =?UTF-8?q?d=20of=20assuming=20main=20(an=20extra's=20configure=20used=20t?= =?UTF-8?q?o=20resize=20the=20main=20surface's=20target),=20and=20the=20sc?= =?UTF-8?q?ale-change=20resize=20moves=20into=20an=20`apply=5Fsurface=5Fsc?= =?UTF-8?q?ale`=20helper=20shared=20by=20main,=20overlays=20and=20the=20lo?= =?UTF-8?q?ck=20extras,=20so=20a=20HiDPI=20secondary=20gets=20a=20correctl?= =?UTF-8?q?y=20scaled=20buffer=20instead=20of=20staying=20at=20factor=201.?= =?UTF-8?q?=20`new=5Foutput`=20creates=20a=20lock=20surface=20for=20an=20o?= =?UTF-8?q?utput=20hotplugged=20while=20locked=20=E2=80=94=20the=20composi?= =?UTF-8?q?tor=20would=20blank=20it=20otherwise=20=E2=80=94=20and=20`outpu?= =?UTF-8?q?t=5Fdestroyed`=20drops=20the=20matching=20extra.=20Input=20on?= =?UTF-8?q?=20the=20extras=20is=20deliberately=20inert,=20with=20one=20asy?= =?UTF-8?q?mmetry:=20pointer=20and=20touch=20events=20landing=20on=20them?= =?UTF-8?q?=20are=20discarded=20rather=20than=20falling=20back=20to=20`Mai?= =?UTF-8?q?n`,=20because=20their=20coordinates=20would=20hit-test=20the=20?= =?UTF-8?q?main=20surface's=20widgets=20and=20a=20click=20on=20a=20seconda?= =?UTF-8?q?ry=20screen=20could=20press=20whatever=20sits=20at=20those=20co?= =?UTF-8?q?ordinates=20on=20the=20form=20=E2=80=94=20the=20power=20button?= =?UTF-8?q?=20included.=20Keyboard=20keeps=20its=20existing=20fall-through?= =?UTF-8?q?=20to=20`Main`,=20so=20typing=20works=20wherever=20the=20compos?= =?UTF-8?q?itor=20decides=20to=20put=20the=20lock=20focus.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.rs | 13 +++ src/event_loop/app_data.rs | 20 +++++ src/event_loop/frame.rs | 27 ++++++ src/event_loop/handlers.rs | 145 +++++++++++++++++++++++++-------- src/event_loop/invalidation.rs | 10 +++ src/event_loop/run.rs | 5 +- src/input/pointer/mod.rs | 1 + src/input/touch/mod.rs | 1 + 8 files changed, 188 insertions(+), 34 deletions(-) diff --git a/src/app.rs b/src/app.rs index 392c1a8..2ee422c 100644 --- a/src/app.rs +++ b/src/app.rs @@ -794,6 +794,19 @@ pub trait App: 'static /// returning `true` from [`requested_exit`](Self::requested_exit). fn shell_mode( &self ) -> ShellMode { ShellMode::Window } + /// Content for the lock surfaces the runtime creates on every output + /// beyond the first while in [`ShellMode::SessionLock`]. The compositor + /// blanks any output without a lock surface, so the runtime covers each + /// one; this view is what gets painted there (typically the same + /// wallpaper as the main surface, without the interactive form). + /// `width` / `height` are that output's surface size in physical pixels. + /// Default `None`: the surface is filled with + /// [`background_color`](Self::background_color). + fn lock_secondary_view( &self, _width: u32, _height: u32 ) -> Option> + { + None + } + /// Return `true` to tear the surface down and exit the event loop. For a /// [`ShellMode::SessionLock`] surface the runtime calls `unlock` first, so /// the compositor lifts the lock instead of leaving the outputs blanked. diff --git a/src/event_loop/app_data.rs b/src/event_loop/app_data.rs index 084f88f..d07de2d 100644 --- a/src/event_loop/app_data.rs +++ b/src/event_loop/app_data.rs @@ -17,6 +17,7 @@ use smithay_client_toolkit::reexports::client:: protocol:: { wl_keyboard::WlKeyboard, + wl_output::WlOutput, wl_pointer::WlPointer, wl_surface::WlSurface, wl_touch::WlTouch, @@ -218,6 +219,15 @@ pub struct AppData /// Populated and reconciled by the run loop from [`App::overlays`]. Always /// empty until overlay diffing is wired up. pub overlays: HashMap>, + /// Session-lock surfaces covering every output beyond the first, paired + /// with the output each one belongs to. The compositor blanks any output + /// without a lock surface, so one is created per output; these render + /// [`App::lock_secondary_view`] and take no input (the input handlers + /// find no focus for them, and keyboard falls through to `Main`). + pub lock_extras: Vec<( WlOutput, SurfaceState )>, + /// Output the main lock surface was created on, so a later `new_output` + /// for the same output does not add a duplicate lock surface. + pub main_lock_output: Option, /// Input-transparent child surfaces keyed by their stable /// [`SubsurfaceId`]. Reconciled each frame from [`App::subsurfaces`]. pub subsurfaces: HashMap, @@ -285,6 +295,16 @@ impl AppData None } + /// Whether `wl` is one of the secondary session-lock surfaces. Pointer and + /// touch events landing on them must be dropped rather than falling back + /// to `Main` — their coordinates would hit-test the main surface's + /// widgets. Keyboard keeps the `Main` fallback so typing works wherever + /// the compositor puts the lock focus. + pub( crate ) fn is_lock_extra_surface( &self, wl: &WlSurface ) -> bool + { + self.lock_extras.iter().any( |( _, ss )| ss.surface.try_wl_surface() == Some( wl ) ) + } + /// Borrow the [`SurfaceState`] identified by `focus`. Panics if `focus` /// refers to an overlay that is not currently registered — callers must /// only pass focus values obtained from [`focus_for_surface`] or from the diff --git a/src/event_loop/frame.rs b/src/event_loop/frame.rs index 6a19c06..64ecdf9 100644 --- a/src/event_loop/frame.rs +++ b/src/event_loop/frame.rs @@ -71,6 +71,33 @@ pub( crate ) fn draw_frame( data: &mut AppData ) -> bool } } + for ( _, ss ) in data.lock_extras.iter_mut() + { + if !ss.configured || !ss.needs_redraw || ss.frame_pending { continue; } + let scale = ss.scale_factor.max( 1 ) as u32; + let view = data.app.lock_secondary_view( ss.width * scale, ss.height * scale ) + .unwrap_or_else( || crate::spacer().into() ); + // No frame callback: these surfaces are static between invalidations, + // so there is no per-frame pacing to keep — and a callback that never + // gets requested again would leave `frame_pending` stuck. + let req_frame = | _: &WlSurface | {}; + draw_surface::( + ss, + &data.compositor_state, + egl_ctx, + &view, + main_bg, + None, + debug_layout, + format, + swap_rb, + &req_frame, + ); + ss.needs_redraw = false; + ss.frame_pending = false; + ss.last_draw = std::time::Instant::now(); + } + for spec in overlays { if let Some( ss ) = data.overlays.get_mut( &spec.id ) diff --git a/src/event_loop/handlers.rs b/src/event_loop/handlers.rs index b7c03b5..25cf14e 100644 --- a/src/event_loop/handlers.rs +++ b/src/event_loop/handlers.rs @@ -45,12 +45,62 @@ use wayland_protocols::wp::text_input::zv3::client:: use crate::app::{ App, ToplevelEvent }; use super::app_data::AppData; +/// Adopt a new buffer scale on `ss`: resize whichever rendering target is +/// active and request a redraw. Returns the new physical size, or `None` +/// when the factor is unchanged. +fn apply_surface_scale( + ss: &mut super::SurfaceState, + shm: &Shm, + surface: &WlSurface, + new_factor: i32, +) -> Option<( u32, u32 )> +{ + if new_factor == ss.scale_factor { return None; } + 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; + // The two targets 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(); + Some( ( pw, ph ) ) +} + 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 Some( focus ) = self.focus_for_surface( surface ) else + { + // Lock extras live outside the focus map. + let shm = &self.shm; + if let Some( ( _, ss ) ) = self.lock_extras.iter_mut() + .find( |( _, ss )| ss.surface.try_wl_surface() == Some( surface ) ) + { + apply_surface_scale( ss, shm, surface, new_factor ); + } + return; + }; let ( pw, ph ) = { let shm = &self.shm; let ss = match focus @@ -62,35 +112,11 @@ impl CompositorHandler for AppData 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 + match apply_surface_scale( ss, shm, surface, new_factor ) { - canvas.set_dpi_scale( new_factor as f32 ); + Some( dims ) => dims, + None => return, } - 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 ) }; // Anchors and baked overlay geometry are in the old scale's // physical pixels — drop the tooltip and rebuild the specs. @@ -270,10 +296,34 @@ impl OutputHandler for AppData ss.surface.materialize( &self.compositor_state, layer_shell, qh, &output ); } } + // An output hotplugged while the session is locked would be blanked by + // the compositor until it gets a lock surface of its own. + if let Some( ref lock ) = self.session_lock + { + if matches!( self.main.surface, super::SurfaceKind::Lock( .. ) ) + && self.main_lock_output.as_ref() != Some( &output ) + && !self.lock_extras.iter().any( |( o, _ )| o == &output ) + { + let surface = self.compositor_state.create_surface( qh ); + let lock_surface = lock.create_lock_surface( surface, &output, qh ); + self.lock_extras.push( ( + output, + super::SurfaceState::new( super::SurfaceKind::Lock( lock_surface ), 0.0, String::new() ), + ) ); + } + } } fn update_output( &mut self, _: &Connection, _: &QueueHandle, _: WlOutput ) {} - fn output_destroyed( &mut self, _: &Connection, _: &QueueHandle, _: WlOutput ) {} + + fn output_destroyed( &mut self, _: &Connection, _: &QueueHandle, output: WlOutput ) + { + self.lock_extras.retain( |( o, _ )| o != &output ); + if self.main_lock_output.as_ref() == Some( &output ) + { + self.main_lock_output = None; + } + } } impl SeatHandler for AppData @@ -432,11 +482,26 @@ impl SessionLockHandler for AppData { fn locked( &mut self, _conn: &Connection, qh: &QueueHandle, session_lock: SessionLock ) { - if let Some( output ) = self.output_state.outputs().next() + // Every output needs its own lock surface — the compositor blanks any + // output left without one. The first hosts the app's main surface; + // the rest render `App::lock_secondary_view`. + let outputs: Vec = self.output_state.outputs().collect(); + let mut outputs = outputs.into_iter(); + if let Some( output ) = 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.main.surface = super::SurfaceKind::Lock( lock_surface ); + self.main_lock_output = Some( output ); + } + for output in outputs + { + let surface = self.compositor_state.create_surface( qh ); + let lock_surface = session_lock.create_lock_surface( surface, &output, qh ); + self.lock_extras.push( ( + output, + super::SurfaceState::new( super::SurfaceKind::Lock( lock_surface ), 0.0, String::new() ), + ) ); } self.session_lock = Some( session_lock ); } @@ -451,13 +516,27 @@ impl SessionLockHandler for AppData &mut self, _conn: &Connection, _qh: &QueueHandle, - _surface: SessionLockSurface, + surface: SessionLockSurface, configure: SessionLockSurfaceConfigure, _serial: u32, ) { let ( w, h ) = configure.new_size; - self.on_configure( w.max( 1 ), h.max( 1 ) ); + let ( w, h ) = ( w.max( 1 ), h.max( 1 ) ); + let wl = surface.wl_surface(); + if self.main.surface.try_wl_surface() == Some( wl ) + { + self.on_configure( w, h ); + return; + } + for ( _, ss ) in self.lock_extras.iter_mut() + { + if ss.surface.try_wl_surface() == Some( wl ) + { + ss.on_configure( &self.shm, self.egl_context.as_ref(), w, h ); + return; + } + } } } diff --git a/src/event_loop/invalidation.rs b/src/event_loop/invalidation.rs index 9ba63b8..bfcced0 100644 --- a/src/event_loop/invalidation.rs +++ b/src/event_loop/invalidation.rs @@ -27,6 +27,10 @@ pub( super ) fn apply_invalidation( data: &mut AppData, scope: Invali { ss.request_redraw(); } + for ( _, ss ) in data.lock_extras.iter_mut() + { + ss.request_redraw(); + } } InvalidationScope::Only( targets ) => { @@ -38,6 +42,12 @@ pub( super ) fn apply_invalidation( data: &mut AppData, scope: Invali { data.view_dirty = true; data.main.request_redraw(); + // Lock extras mirror the main surface's backdrop, so + // they follow its invalidations. + for ( _, ss ) in data.lock_extras.iter_mut() + { + ss.request_redraw(); + } } SurfaceTarget::Overlay( id ) => { diff --git a/src/event_loop/run.rs b/src/event_loop/run.rs index ec9d2c7..0917681 100644 --- a/src/event_loop/run.rs +++ b/src/event_loop/run.rs @@ -320,6 +320,8 @@ pub( crate ) fn try_run( app: A ) -> Result<(), RunError> pending_size_hint_unpin, main: SurfaceState::::new( surface_kind, titlebar_height, titlebar_title ), overlays: std::collections::HashMap::new(), + lock_extras: Vec::new(), + main_lock_output: None, subsurfaces: std::collections::HashMap::new(), subsurface_gles_canvas: None, pointer_focus: SurfaceFocus::Main, @@ -597,7 +599,8 @@ pub( crate ) fn try_run( app: A ) -> Result<(), RunError> // on a frame callback. The compositor decides our cadence — when no // surface qualifies we just loop back to `dispatch(None)` and sleep. let any_drawable = ( data.main.configured && data.main.needs_redraw && !data.main.frame_pending ) - || data.overlays.values().any( |ss| ss.configured && ss.needs_redraw && !ss.frame_pending ); + || data.overlays.values().any( |ss| ss.configured && ss.needs_redraw && !ss.frame_pending ) + || data.lock_extras.iter().any( |( _, ss )| ss.configured && ss.needs_redraw && !ss.frame_pending ); if any_drawable { // Rebuild while motion is in progress and on the first frame diff --git a/src/input/pointer/mod.rs b/src/input/pointer/mod.rs index 913b0dd..5a02c5a 100644 --- a/src/input/pointer/mod.rs +++ b/src/input/pointer/mod.rs @@ -51,6 +51,7 @@ impl PointerHandler for AppData { for event in events { + if self.is_lock_extra_surface( &event.surface ) { continue; } let focus = self.focus_for_surface( &event.surface ) .unwrap_or( SurfaceFocus::Main ); self.pointer_focus = focus; diff --git a/src/input/touch/mod.rs b/src/input/touch/mod.rs index 8c4df53..31a6b12 100644 --- a/src/input/touch/mod.rs +++ b/src/input/touch/mod.rs @@ -60,6 +60,7 @@ impl TouchHandler for AppData ) { self.last_input_serial = serial; + if self.is_lock_extra_surface( &surface ) { return; } let focus = self.focus_for_surface( &surface ).unwrap_or( SurfaceFocus::Main ); self.touch_focus.insert( id, focus ); let pos = self.surface( focus ).to_physical( position.0, position.1 );