From 78053b8b012beb25e4bae0a3df98000d0aa47135 Mon Sep 17 00:00:00 2001 From: "Pedro M. de Echanove Pasquin" Date: Fri, 14 Aug 2026 23:20:58 +0200 Subject: [PATCH] =?UTF-8?q?Cover=20every=20output=20from=20a=20background?= =?UTF-8?q?=20shell:=20the=20session-lock=20secondary=20machinery=20genera?= =?UTF-8?q?lizes=20to=20layer=20surfaces=20The=20runtime=20already=20cover?= =?UTF-8?q?ed=20outputs=20beyond=20the=20first,=20but=20only=20for=20`Shel?= =?UTF-8?q?lMode::SessionLock`:=20`locked`=20created=20one=20`ext-session-?= =?UTF-8?q?lock`=20surface=20per=20output,=20tracked=20in=20`lock=5Fextras?= =?UTF-8?q?`,=20rendered=20from=20`App::lock=5Fsecondary=5Fview`,=20exclud?= =?UTF-8?q?ed=20from=20pointer/touch=20routing=20and=20repainted=20through?= =?UTF-8?q?=20the=20shared=20invalidation=20path.=20A=20background=20shell?= =?UTF-8?q?=20had=20nothing=20equivalent,=20so=20on=20a=20multi-monitor=20?= =?UTF-8?q?session=20its=20wallpaper=20simply=20did=20not=20exist=20on=20t?= =?UTF-8?q?he=20extended=20screens=20=E2=80=94=20the=20compositor=20had=20?= =?UTF-8?q?to=20fake=20one=20by=20cover-scaling=20the=20primary's=20backgr?= =?UTF-8?q?ound,=20wrong=20scale=20and=20wrong=20crop=20included.=20The=20?= =?UTF-8?q?machinery=20is=20shared=20rather=20than=20duplicated.=20The=20s?= =?UTF-8?q?tate=20and=20the=20hook=20lose=20their=20lock-specific=20names?= =?UTF-8?q?=20=E2=80=94=20`lock=5Fextras`=20becomes=20`secondaries`,=20`ma?= =?UTF-8?q?in=5Flock=5Foutput`=20becomes=20`main=5Foutput`,=20`App::lock?= =?UTF-8?q?=5Fsecondary=5Fview`=20becomes=20`App::secondary=5Fview`=20?= =?UTF-8?q?=E2=80=94=20and=20everything=20downstream=20of=20creation=20(th?= =?UTF-8?q?e=20draw=20loop=20in=20frame.rs,=20invalidation,=20per-surface?= =?UTF-8?q?=20scale=20handling,=20the=20input=20exclusion=20in=20pointer/t?= =?UTF-8?q?ouch,=20redraw=20scheduling=20in=20run.rs)=20is=20the=20same=20?= =?UTF-8?q?single=20copy=20serving=20both=20kinds.=20What=20is=20genuinely?= =?UTF-8?q?=20new=20is=20the=20second=20creation=20site:=20in=20`new=5Fout?= =?UTF-8?q?put`,=20when=20the=20app's=20shell=20mode=20is=20`Layer(=20Back?= =?UTF-8?q?ground=20)`,=20every=20output=20beyond=20the=20main=20surface's?= =?UTF-8?q?=20gets=20its=20own=20background=20layer=20surface=20=E2=80=94?= =?UTF-8?q?=20anchored=20to=20all=20four=20edges=20with=20size=20delegated?= =?UTF-8?q?=20to=20the=20compositor,=20exclusive=20zone=200,=20no=20keyboa?= =?UTF-8?q?rd=20interactivity=20=E2=80=94=20bound=20to=20that=20output=20a?= =?UTF-8?q?t=20creation=20and=20torn=20down=20in=20`output=5Fdestroyed`.?= =?UTF-8?q?=20The=20main=20surface's=20output=20is=20now=20recorded=20when?= =?UTF-8?q?=20it=20materializes=20so=20the=20runtime=20knows=20which=20out?= =?UTF-8?q?put=20not=20to=20cover.=20Layer-shell=20events=20learn=20about?= =?UTF-8?q?=20secondaries=20too,=20and=20the=20`configure`=20branch=20is?= =?UTF-8?q?=20not=20optional:=20secondaries=20live=20outside=20the=20focus?= =?UTF-8?q?=20map,=20so=20their=20configure=20used=20to=20fall=20into=20th?= =?UTF-8?q?e=20`None`=20arm=20and=20resize=20the=20main=20surface.=20`clos?= =?UTF-8?q?ed`=20on=20a=20secondary=20now=20just=20drops=20it=20instead=20?= =?UTF-8?q?of=20asking=20the=20app=20to=20exit.=20`secondary=5Fview`=20kee?= =?UTF-8?q?ps=20its=20default=20of=20`None`=20(surface=20filled=20with=20`?= =?UTF-8?q?background=5Fcolor`),=20so=20nothing=20changes=20for=20apps=20t?= =?UTF-8?q?hat=20do=20not=20opt=20in;=20the=20session-lock=20behaviour=20i?= =?UTF-8?q?s=20unchanged=20aside=20from=20the=20renames.?= 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 | 28 +++++++------ src/event_loop/frame.rs | 4 +- src/event_loop/handlers.rs | 77 +++++++++++++++++++++++++++------- src/event_loop/invalidation.rs | 4 +- src/event_loop/run.rs | 6 +-- src/input/pointer/mod.rs | 2 +- src/input/touch/mod.rs | 2 +- 8 files changed, 94 insertions(+), 42 deletions(-) diff --git a/src/app.rs b/src/app.rs index 2ee422c..e006e82 100644 --- a/src/app.rs +++ b/src/app.rs @@ -794,15 +794,16 @@ 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). + /// Content for the surfaces the runtime creates on every output beyond + /// the first. In [`ShellMode::SessionLock`] these are lock surfaces — + /// the compositor blanks any output without one, so each is covered. In + /// [`ShellMode::Layer`] on [`Layer::Background`] they are background + /// layer surfaces, so a shell's wallpaper reaches extended outputs + /// (typically without the grid or any chrome). They take no input. /// `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> + fn secondary_view( &self, _width: u32, _height: u32 ) -> Option> { None } diff --git a/src/event_loop/app_data.rs b/src/event_loop/app_data.rs index d07de2d..42b07ca 100644 --- a/src/event_loop/app_data.rs +++ b/src/event_loop/app_data.rs @@ -219,15 +219,17 @@ 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, + /// Surfaces covering every output beyond the first, paired with the + /// output each one belongs to: session-lock surfaces while locked (the + /// compositor blanks any output without one), background layer surfaces + /// for a [`crate::Layer::Background`] shell (the wallpaper reaches + /// extended outputs). These render [`App::secondary_view`] and take no + /// input (the input handlers find no focus for them, and keyboard falls + /// through to `Main`). + pub secondaries: Vec<( WlOutput, SurfaceState )>, + /// Output the main surface was created on, so a later `new_output` for + /// the same output does not add a duplicate secondary surface. + pub main_output: Option, /// Input-transparent child surfaces keyed by their stable /// [`SubsurfaceId`]. Reconciled each frame from [`App::subsurfaces`]. pub subsurfaces: HashMap, @@ -295,14 +297,14 @@ impl AppData None } - /// Whether `wl` is one of the secondary session-lock surfaces. Pointer and + /// Whether `wl` is one of the secondary per-output 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 + /// the compositor puts the focus. + pub( crate ) fn is_secondary_surface( &self, wl: &WlSurface ) -> bool { - self.lock_extras.iter().any( |( _, ss )| ss.surface.try_wl_surface() == Some( wl ) ) + self.secondaries.iter().any( |( _, ss )| ss.surface.try_wl_surface() == Some( wl ) ) } /// Borrow the [`SurfaceState`] identified by `focus`. Panics if `focus` diff --git a/src/event_loop/frame.rs b/src/event_loop/frame.rs index 64ecdf9..653be6c 100644 --- a/src/event_loop/frame.rs +++ b/src/event_loop/frame.rs @@ -71,11 +71,11 @@ pub( crate ) fn draw_frame( data: &mut AppData ) -> bool } } - for ( _, ss ) in data.lock_extras.iter_mut() + for ( _, ss ) in data.secondaries.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 ) + let view = data.app.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 diff --git a/src/event_loop/handlers.rs b/src/event_loop/handlers.rs index 25cf14e..f618d08 100644 --- a/src/event_loop/handlers.rs +++ b/src/event_loop/handlers.rs @@ -92,9 +92,9 @@ impl CompositorHandler for AppData if new_factor <= 0 { return; } let Some( focus ) = self.focus_for_surface( surface ) else { - // Lock extras live outside the focus map. + // Secondary per-output surfaces live outside the focus map. let shm = &self.shm; - if let Some( ( _, ss ) ) = self.lock_extras.iter_mut() + if let Some( ( _, ss ) ) = self.secondaries.iter_mut() .find( |( _, ss )| ss.surface.try_wl_surface() == Some( surface ) ) { apply_surface_scale( ss, shm, surface, new_factor ); @@ -165,6 +165,11 @@ impl LayerShellHandler for AppData layer: &LayerSurface, ) { + if self.is_secondary_surface( layer.wl_surface() ) + { + self.secondaries.retain( |( _, ss )| ss.surface.try_wl_surface() != Some( layer.wl_surface() ) ); + return; + } match self.focus_for_surface( layer.wl_surface() ) { Some( super::SurfaceFocus::Main ) | None => @@ -200,6 +205,17 @@ impl LayerShellHandler for AppData { let ( w, h ) = configure.new_size; let ( w, h ) = ( w.max( 1 ), h.max( 1 ) ); + // Secondaries live outside the focus map; without this branch their + // configure would fall into the `None` arm and resize the main + // surface instead. + for ( _, ss ) in self.secondaries.iter_mut() + { + if ss.surface.try_wl_surface() == Some( layer.wl_surface() ) + { + ss.on_configure( &self.shm, self.egl_context.as_ref(), w, h ); + return; + } + } match self.focus_for_surface( layer.wl_surface() ) { Some( super::SurfaceFocus::Main ) | None => @@ -290,23 +306,56 @@ impl OutputHandler for AppData // 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 ); + if self.main.surface.materialize( &self.compositor_state, layer_shell, qh, &output ) + { + self.main_output = Some( output.clone() ); + } for ss in self.overlays.values_mut() { ss.surface.materialize( &self.compositor_state, layer_shell, qh, &output ); } + // A background shell covers every output: each one beyond the + // main's gets its own background layer surface, painted with + // `App::secondary_view` — the wallpaper without any chrome. + if self.app.shell_mode() == crate::app::ShellMode::Layer( crate::app::Layer::Background ) + && self.main_output.is_some() + && self.main_output.as_ref() != Some( &output ) + && !self.secondaries.iter().any( |( o, _ )| o == &output ) + { + use smithay_client_toolkit::shell::wlr_layer; + let surface = self.compositor_state.create_surface( qh ); + let layer_surface = layer_shell.create_layer_surface( + qh, + surface, + wlr_layer::Layer::Background, + Some( "ltk-secondary" ), + Some( &output ), + ); + layer_surface.set_anchor( + wlr_layer::Anchor::TOP | wlr_layer::Anchor::BOTTOM + | wlr_layer::Anchor::LEFT | wlr_layer::Anchor::RIGHT, + ); + layer_surface.set_exclusive_zone( 0 ); + layer_surface.set_keyboard_interactivity( wlr_layer::KeyboardInteractivity::None ); + layer_surface.set_size( 0, 0 ); + layer_surface.commit(); + self.secondaries.push( ( + output.clone(), + super::SurfaceState::new( super::SurfaceKind::Layer( layer_surface ), 0.0, String::new() ), + ) ); + } } // 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 ) + && self.main_output.as_ref() != Some( &output ) + && !self.secondaries.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( ( + self.secondaries.push( ( output, super::SurfaceState::new( super::SurfaceKind::Lock( lock_surface ), 0.0, String::new() ), ) ); @@ -318,10 +367,10 @@ impl OutputHandler for AppData 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.secondaries.retain( |( o, _ )| o != &output ); + if self.main_output.as_ref() == Some( &output ) { - self.main_lock_output = None; + self.main_output = None; } } } @@ -484,21 +533,21 @@ impl SessionLockHandler for AppData { // 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`. + // the rest render `App::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_lock_output = Some( output ); + self.main.surface = super::SurfaceKind::Lock( lock_surface ); + self.main_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( ( + self.secondaries.push( ( output, super::SurfaceState::new( super::SurfaceKind::Lock( lock_surface ), 0.0, String::new() ), ) ); @@ -529,7 +578,7 @@ impl SessionLockHandler for AppData self.on_configure( w, h ); return; } - for ( _, ss ) in self.lock_extras.iter_mut() + for ( _, ss ) in self.secondaries.iter_mut() { if ss.surface.try_wl_surface() == Some( wl ) { diff --git a/src/event_loop/invalidation.rs b/src/event_loop/invalidation.rs index bfcced0..7320fef 100644 --- a/src/event_loop/invalidation.rs +++ b/src/event_loop/invalidation.rs @@ -27,7 +27,7 @@ pub( super ) fn apply_invalidation( data: &mut AppData, scope: Invali { ss.request_redraw(); } - for ( _, ss ) in data.lock_extras.iter_mut() + for ( _, ss ) in data.secondaries.iter_mut() { ss.request_redraw(); } @@ -44,7 +44,7 @@ pub( super ) fn apply_invalidation( data: &mut AppData, scope: Invali 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() + for ( _, ss ) in data.secondaries.iter_mut() { ss.request_redraw(); } diff --git a/src/event_loop/run.rs b/src/event_loop/run.rs index 0917681..6121bc0 100644 --- a/src/event_loop/run.rs +++ b/src/event_loop/run.rs @@ -320,8 +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, + secondaries: Vec::new(), + main_output: None, subsurfaces: std::collections::HashMap::new(), subsurface_gles_canvas: None, pointer_focus: SurfaceFocus::Main, @@ -600,7 +600,7 @@ pub( crate ) fn try_run( app: A ) -> Result<(), RunError> // 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.lock_extras.iter().any( |( _, ss )| ss.configured && ss.needs_redraw && !ss.frame_pending ); + || data.secondaries.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 5a02c5a..cfaa850 100644 --- a/src/input/pointer/mod.rs +++ b/src/input/pointer/mod.rs @@ -51,7 +51,7 @@ impl PointerHandler for AppData { for event in events { - if self.is_lock_extra_surface( &event.surface ) { continue; } + if self.is_secondary_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 31a6b12..1890082 100644 --- a/src/input/touch/mod.rs +++ b/src/input/touch/mod.rs @@ -60,7 +60,7 @@ impl TouchHandler for AppData ) { self.last_input_serial = serial; - if self.is_lock_extra_surface( &surface ) { return; } + if self.is_secondary_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 );