Cover every output from a background shell: the session-lock secondary machinery generalizes to layer surfaces
Some checks failed
CI / build + test (push) Has been cancelled
CI / cargo audit (push) Has been cancelled

The runtime already covered outputs beyond the first, but only for `ShellMode::SessionLock`: `locked` created one `ext-session-lock` surface per output, tracked in `lock_extras`, rendered from `App::lock_secondary_view`, excluded from pointer/touch routing and repainted through the shared invalidation path. A background shell had nothing equivalent, so on a multi-monitor session its wallpaper simply did not exist on the extended screens — the compositor had to fake one by cover-scaling the primary's background, wrong scale and wrong crop included.
The machinery is shared rather than duplicated. The state and the hook lose their lock-specific names — `lock_extras` becomes `secondaries`, `main_lock_output` becomes `main_output`, `App::lock_secondary_view` becomes `App::secondary_view` — and everything downstream of creation (the draw loop in frame.rs, invalidation, per-surface scale handling, the input exclusion in pointer/touch, redraw scheduling in run.rs) is the same single copy serving both kinds.
What is genuinely new is the second creation site: in `new_output`, when the app's shell mode is `Layer( Background )`, every output beyond the main surface's gets its own background layer surface — anchored to all four edges with size delegated to the compositor, exclusive zone 0, no keyboard interactivity — bound to that output at creation and torn down in `output_destroyed`. The main surface's output is now recorded when it materializes so the runtime knows which output not to cover.
Layer-shell events learn about secondaries too, and the `configure` branch is not optional: secondaries live outside the focus map, so their configure used to fall into the `None` arm and resize the main surface. `closed` on a secondary now just drops it instead of asking the app to exit.
`secondary_view` keeps its default of `None` (surface filled with `background_color`), so nothing changes for apps that do not opt in; the session-lock behaviour is unchanged aside from the renames.
This commit is contained in:
2026-08-14 23:20:58 +02:00
parent 99a4767815
commit 78053b8b01
8 changed files with 94 additions and 42 deletions

View File

@@ -794,15 +794,16 @@ pub trait App: 'static
/// returning `true` from [`requested_exit`](Self::requested_exit). /// returning `true` from [`requested_exit`](Self::requested_exit).
fn shell_mode( &self ) -> ShellMode { ShellMode::Window } fn shell_mode( &self ) -> ShellMode { ShellMode::Window }
/// Content for the lock surfaces the runtime creates on every output /// Content for the surfaces the runtime creates on every output beyond
/// beyond the first while in [`ShellMode::SessionLock`]. The compositor /// the first. In [`ShellMode::SessionLock`] these are lock surfaces —
/// blanks any output without a lock surface, so the runtime covers each /// the compositor blanks any output without one, so each is covered. In
/// one; this view is what gets painted there (typically the same /// [`ShellMode::Layer`] on [`Layer::Background`] they are background
/// wallpaper as the main surface, without the interactive form). /// 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. /// `width` / `height` are that output's surface size in physical pixels.
/// Default `None`: the surface is filled with /// Default `None`: the surface is filled with
/// [`background_color`](Self::background_color). /// [`background_color`](Self::background_color).
fn lock_secondary_view( &self, _width: u32, _height: u32 ) -> Option<Element<Self::Message>> fn secondary_view( &self, _width: u32, _height: u32 ) -> Option<Element<Self::Message>>
{ {
None None
} }

View File

@@ -219,15 +219,17 @@ pub struct AppData<A: App>
/// Populated and reconciled by the run loop from [`App::overlays`]. Always /// Populated and reconciled by the run loop from [`App::overlays`]. Always
/// empty until overlay diffing is wired up. /// empty until overlay diffing is wired up.
pub overlays: HashMap<OverlayId, SurfaceState<A::Message>>, pub overlays: HashMap<OverlayId, SurfaceState<A::Message>>,
/// Session-lock surfaces covering every output beyond the first, paired /// Surfaces covering every output beyond the first, paired with the
/// with the output each one belongs to. The compositor blanks any output /// output each one belongs to: session-lock surfaces while locked (the
/// without a lock surface, so one is created per output; these render /// compositor blanks any output without one), background layer surfaces
/// [`App::lock_secondary_view`] and take no input (the input handlers /// for a [`crate::Layer::Background`] shell (the wallpaper reaches
/// find no focus for them, and keyboard falls through to `Main`). /// extended outputs). These render [`App::secondary_view`] and take no
pub lock_extras: Vec<( WlOutput, SurfaceState<A::Message> )>, /// input (the input handlers find no focus for them, and keyboard falls
/// Output the main lock surface was created on, so a later `new_output` /// through to `Main`).
/// for the same output does not add a duplicate lock surface. pub secondaries: Vec<( WlOutput, SurfaceState<A::Message> )>,
pub main_lock_output: Option<WlOutput>, /// 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<WlOutput>,
/// Input-transparent child surfaces keyed by their stable /// Input-transparent child surfaces keyed by their stable
/// [`SubsurfaceId`]. Reconciled each frame from [`App::subsurfaces`]. /// [`SubsurfaceId`]. Reconciled each frame from [`App::subsurfaces`].
pub subsurfaces: HashMap<SubsurfaceId, SubsurfaceSlot>, pub subsurfaces: HashMap<SubsurfaceId, SubsurfaceSlot>,
@@ -295,14 +297,14 @@ impl<A: App> AppData<A>
None 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 /// touch events landing on them must be dropped rather than falling back
/// to `Main` — their coordinates would hit-test the main surface's /// to `Main` — their coordinates would hit-test the main surface's
/// widgets. Keyboard keeps the `Main` fallback so typing works wherever /// widgets. Keyboard keeps the `Main` fallback so typing works wherever
/// the compositor puts the lock focus. /// the compositor puts the focus.
pub( crate ) fn is_lock_extra_surface( &self, wl: &WlSurface ) -> bool 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` /// Borrow the [`SurfaceState`] identified by `focus`. Panics if `focus`

View File

@@ -71,11 +71,11 @@ pub( crate ) fn draw_frame<A: App>( data: &mut AppData<A> ) -> 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; } if !ss.configured || !ss.needs_redraw || ss.frame_pending { continue; }
let scale = ss.scale_factor.max( 1 ) as u32; 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() ); .unwrap_or_else( || crate::spacer().into() );
// No frame callback: these surfaces are static between invalidations, // No frame callback: these surfaces are static between invalidations,
// so there is no per-frame pacing to keep — and a callback that never // so there is no per-frame pacing to keep — and a callback that never

View File

@@ -92,9 +92,9 @@ impl<A: App> CompositorHandler for AppData<A>
if new_factor <= 0 { return; } if new_factor <= 0 { return; }
let Some( focus ) = self.focus_for_surface( surface ) else 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; 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 ) ) .find( |( _, ss )| ss.surface.try_wl_surface() == Some( surface ) )
{ {
apply_surface_scale( ss, shm, surface, new_factor ); apply_surface_scale( ss, shm, surface, new_factor );
@@ -165,6 +165,11 @@ impl<A: App> LayerShellHandler for AppData<A>
layer: &LayerSurface, 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() ) match self.focus_for_surface( layer.wl_surface() )
{ {
Some( super::SurfaceFocus::Main ) | None => Some( super::SurfaceFocus::Main ) | None =>
@@ -200,6 +205,17 @@ impl<A: App> LayerShellHandler for AppData<A>
{ {
let ( w, h ) = configure.new_size; let ( w, h ) = configure.new_size;
let ( w, h ) = ( w.max( 1 ), h.max( 1 ) ); 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() ) match self.focus_for_surface( layer.wl_surface() )
{ {
Some( super::SurfaceFocus::Main ) | None => Some( super::SurfaceFocus::Main ) | None =>
@@ -290,23 +306,56 @@ impl<A: App> OutputHandler for AppData<A>
// Same for any overlays that were created before an output existed. // Same for any overlays that were created before an output existed.
if let Some( ref layer_shell ) = self.layer_shell 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() for ss in self.overlays.values_mut()
{ {
ss.surface.materialize( &self.compositor_state, layer_shell, qh, &output ); 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 // An output hotplugged while the session is locked would be blanked by
// the compositor until it gets a lock surface of its own. // the compositor until it gets a lock surface of its own.
if let Some( ref lock ) = self.session_lock if let Some( ref lock ) = self.session_lock
{ {
if matches!( self.main.surface, super::SurfaceKind::Lock( .. ) ) if matches!( self.main.surface, super::SurfaceKind::Lock( .. ) )
&& self.main_lock_output.as_ref() != Some( &output ) && self.main_output.as_ref() != Some( &output )
&& !self.lock_extras.iter().any( |( o, _ )| o == &output ) && !self.secondaries.iter().any( |( o, _ )| o == &output )
{ {
let surface = self.compositor_state.create_surface( qh ); let surface = self.compositor_state.create_surface( qh );
let lock_surface = lock.create_lock_surface( surface, &output, qh ); let lock_surface = lock.create_lock_surface( surface, &output, qh );
self.lock_extras.push( ( self.secondaries.push( (
output, output,
super::SurfaceState::new( super::SurfaceKind::Lock( lock_surface ), 0.0, String::new() ), super::SurfaceState::new( super::SurfaceKind::Lock( lock_surface ), 0.0, String::new() ),
) ); ) );
@@ -318,10 +367,10 @@ impl<A: App> OutputHandler for AppData<A>
fn output_destroyed( &mut self, _: &Connection, _: &QueueHandle<Self>, output: WlOutput ) fn output_destroyed( &mut self, _: &Connection, _: &QueueHandle<Self>, output: WlOutput )
{ {
self.lock_extras.retain( |( o, _ )| o != &output ); self.secondaries.retain( |( o, _ )| o != &output );
if self.main_lock_output.as_ref() == Some( &output ) if self.main_output.as_ref() == Some( &output )
{ {
self.main_lock_output = None; self.main_output = None;
} }
} }
} }
@@ -484,21 +533,21 @@ impl<A: App> SessionLockHandler for AppData<A>
{ {
// Every output needs its own lock surface — the compositor blanks any // Every output needs its own lock surface — the compositor blanks any
// output left without one. The first hosts the app's main surface; // 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<WlOutput> = self.output_state.outputs().collect(); let outputs: Vec<WlOutput> = self.output_state.outputs().collect();
let mut outputs = outputs.into_iter(); let mut outputs = outputs.into_iter();
if let Some( output ) = outputs.next() if let Some( output ) = outputs.next()
{ {
let surface = self.compositor_state.create_surface( qh ); let surface = self.compositor_state.create_surface( qh );
let lock_surface = session_lock.create_lock_surface( surface, &output, 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 ); self.main_output = Some( output );
} }
for output in outputs for output in outputs
{ {
let surface = self.compositor_state.create_surface( qh ); let surface = self.compositor_state.create_surface( qh );
let lock_surface = session_lock.create_lock_surface( surface, &output, qh ); let lock_surface = session_lock.create_lock_surface( surface, &output, qh );
self.lock_extras.push( ( self.secondaries.push( (
output, output,
super::SurfaceState::new( super::SurfaceKind::Lock( lock_surface ), 0.0, String::new() ), super::SurfaceState::new( super::SurfaceKind::Lock( lock_surface ), 0.0, String::new() ),
) ); ) );
@@ -529,7 +578,7 @@ impl<A: App> SessionLockHandler for AppData<A>
self.on_configure( w, h ); self.on_configure( w, h );
return; return;
} }
for ( _, ss ) in self.lock_extras.iter_mut() for ( _, ss ) in self.secondaries.iter_mut()
{ {
if ss.surface.try_wl_surface() == Some( wl ) if ss.surface.try_wl_surface() == Some( wl )
{ {

View File

@@ -27,7 +27,7 @@ pub( super ) fn apply_invalidation<A: App>( data: &mut AppData<A>, scope: Invali
{ {
ss.request_redraw(); ss.request_redraw();
} }
for ( _, ss ) in data.lock_extras.iter_mut() for ( _, ss ) in data.secondaries.iter_mut()
{ {
ss.request_redraw(); ss.request_redraw();
} }
@@ -44,7 +44,7 @@ pub( super ) fn apply_invalidation<A: App>( data: &mut AppData<A>, scope: Invali
data.main.request_redraw(); data.main.request_redraw();
// Lock extras mirror the main surface's backdrop, so // Lock extras mirror the main surface's backdrop, so
// they follow its invalidations. // they follow its invalidations.
for ( _, ss ) in data.lock_extras.iter_mut() for ( _, ss ) in data.secondaries.iter_mut()
{ {
ss.request_redraw(); ss.request_redraw();
} }

View File

@@ -320,8 +320,8 @@ pub( crate ) fn try_run<A: App>( app: A ) -> Result<(), RunError>
pending_size_hint_unpin, pending_size_hint_unpin,
main: SurfaceState::<A::Message>::new( surface_kind, titlebar_height, titlebar_title ), main: SurfaceState::<A::Message>::new( surface_kind, titlebar_height, titlebar_title ),
overlays: std::collections::HashMap::new(), overlays: std::collections::HashMap::new(),
lock_extras: Vec::new(), secondaries: Vec::new(),
main_lock_output: None, main_output: None,
subsurfaces: std::collections::HashMap::new(), subsurfaces: std::collections::HashMap::new(),
subsurface_gles_canvas: None, subsurface_gles_canvas: None,
pointer_focus: SurfaceFocus::Main, pointer_focus: SurfaceFocus::Main,
@@ -600,7 +600,7 @@ pub( crate ) fn try_run<A: App>( app: A ) -> Result<(), RunError>
// surface qualifies we just loop back to `dispatch(None)` and sleep. // 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 ) 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 ); || data.secondaries.iter().any( |( _, ss )| ss.configured && ss.needs_redraw && !ss.frame_pending );
if any_drawable if any_drawable
{ {
// Rebuild while motion is in progress and on the first frame // Rebuild while motion is in progress and on the first frame

View File

@@ -51,7 +51,7 @@ impl<A: App> PointerHandler for AppData<A>
{ {
for event in events 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 ) let focus = self.focus_for_surface( &event.surface )
.unwrap_or( SurfaceFocus::Main ); .unwrap_or( SurfaceFocus::Main );
self.pointer_focus = focus; self.pointer_focus = focus;

View File

@@ -60,7 +60,7 @@ impl<A: App> TouchHandler for AppData<A>
) )
{ {
self.last_input_serial = serial; 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 ); let focus = self.focus_for_surface( &surface ).unwrap_or( SurfaceFocus::Main );
self.touch_focus.insert( id, focus ); self.touch_focus.insert( id, focus );
let pos = self.surface( focus ).to_physical( position.0, position.1 ); let pos = self.surface( focus ).to_physical( position.0, position.1 );