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 );