diff --git a/README.md b/README.md index aadea19..f91138f 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ model is "public Wayland toolkit with production consumers" rather than - low idle wakeups and event-driven redraws - partial redraws and damage tracking - a simple declarative tree instead of retained widgets -- direct support for normal windows and layer-shell surfaces +- direct support for normal windows, layer-shell, and ext-session-lock surfaces - a runtime-free core (`ltk::core::UiSurface`) for embedding layout and drawing without `ltk::run()` @@ -230,6 +230,11 @@ Switch to layer-shell only when you are building shell surfaces such as: - greeters - lock screens +For a screen locker, use `ShellMode::SessionLock` instead of layer-shell: it +presents an `ext-session-lock-v1` surface that the compositor keeps on top of +everything until the app returns `true` from `requested_exit()`, which makes +the runtime call `unlock` and lift the lock. + ## Performance Notes `ltk` is designed to sleep when idle and redraw only on real work. diff --git a/src/app.rs b/src/app.rs index af58467..8679d1b 100644 --- a/src/app.rs +++ b/src/app.rs @@ -33,6 +33,11 @@ pub enum ShellMode /// Layer-shell surface at the specified layer. /// Used for shell components like panels, backgrounds, overlays. Layer( Layer ), + + /// `ext-session-lock-v1` lock surface. The compositor blanks the outputs + /// and shows only this surface until the app requests exit (see + /// [`App::requested_exit`]). Used by the screen locker. + SessionLock, } /// Layer-shell layer position. @@ -641,11 +646,20 @@ pub trait App: 'static /// /// - [`ShellMode::Window`]: Normal application window (xdg-shell). **Default.** /// - [`ShellMode::Layer`]: System component at a specific layer (layer-shell). + /// - [`ShellMode::SessionLock`]: `ext-session-lock-v1` lock surface (screen locker). /// /// For regular applications, use the default `Window` mode. /// For shell components (panels, backgrounds, overlays), use `Layer`. + /// For a screen locker, use `SessionLock` and request the unlock by + /// returning `true` from [`requested_exit`](Self::requested_exit). fn shell_mode( &self ) -> ShellMode { ShellMode::Window } + /// 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. + /// Polled after every batch of `update`s. + fn requested_exit( &self ) -> bool { false } + /// Suggest an initial size for an xdg-shell window in logical pixels. /// /// Returning `Some(( w, h ))` makes ltk call both diff --git a/src/event_loop/app_data.rs b/src/event_loop/app_data.rs index 61a7cf7..cfc327b 100644 --- a/src/event_loop/app_data.rs +++ b/src/event_loop/app_data.rs @@ -9,6 +9,7 @@ use smithay_client_toolkit:: seat::SeatState, shell::{ wlr_layer::LayerShell, xdg::XdgShell }, shm::Shm, + session_lock::SessionLock, }; use smithay_client_toolkit::reexports::client:: { @@ -45,6 +46,7 @@ pub struct AppData pub output_state: OutputState, pub compositor_state: CompositorState, pub shm: Shm, + pub session_lock: Option, /// Process-wide EGL context (display + GLES context). `None` when EGL /// failed to initialise or `LTK_FORCE_SOFTWARE=1` — every surface then /// falls back to the SHM path. diff --git a/src/event_loop/handlers.rs b/src/event_loop/handlers.rs index f78203c..221228b 100644 --- a/src/event_loop/handlers.rs +++ b/src/event_loop/handlers.rs @@ -7,7 +7,7 @@ use smithay_client_toolkit:: delegate_compositor, 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_xdg_shell, delegate_xdg_window, delegate_session_lock, foreign_toplevel_list::{ ForeignToplevelList, ForeignToplevelListHandler }, output::{ OutputHandler, OutputState }, registry::{ ProvidesRegistryState, RegistryState }, @@ -22,6 +22,7 @@ use smithay_client_toolkit:: 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:: @@ -396,6 +397,39 @@ impl PopupHandler for AppData // --- 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_output!( @ AppData ); delegate_shm!( @ AppData ); @@ -404,6 +438,7 @@ 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 ); diff --git a/src/event_loop/run.rs b/src/event_loop/run.rs index d95c4ca..107402f 100644 --- a/src/event_loop/run.rs +++ b/src/event_loop/run.rs @@ -143,6 +143,11 @@ pub( crate ) fn try_run( app: A ) -> Result<(), RunError> use crate::app::ShellMode; match app.shell_mode() { + ShellMode::SessionLock => { + // Lock surface is created in SessionLockHandler::locked once the + // compositor grants the lock; until then a placeholder. + ( SurfaceKind::PendingLock, None ) + } ShellMode::Window => { let xdg = bind_xdg( &globals, &qh )?; let surface = compositor.create_surface( &qh ); @@ -183,6 +188,20 @@ pub( crate ) fn try_run( app: A ) -> Result<(), RunError> } }; + // Bind the session-lock manager and request the lock. We don't keep the + // `SessionLockState` (the manager) around: it has no `Drop`, so the + // manager object persists in the connection, and the lock lifecycle runs + // entirely off the returned `SessionLock` + its surfaces. + let session_lock = + if force_window.is_none() && matches!( app.shell_mode(), crate::app::ShellMode::SessionLock ) + { + smithay_client_toolkit::session_lock::SessionLockState::new( &globals, &qh ) + .lock( &qh ) + .ok() + } else { + None + }; + let text_input_manager: Option = globals.bind( &qh, 1..=1, () ).ok(); // wl_data_device_manager: optional. Required for cross-application // copy/paste; absence means the clipboard stays process-local. @@ -243,6 +262,7 @@ pub( crate ) fn try_run( app: A ) -> Result<(), RunError> output_state: OutputState::new( &globals, &qh ), compositor_state: compositor, shm, + session_lock, egl_context, xdg_shell, layer_shell: layer_shell_opt, @@ -459,6 +479,20 @@ pub( crate ) fn try_run( app: A ) -> Result<(), RunError> let pf = data.pointer_focus; data.dispatch_cursor_shape( pf ); } + + // App asked to exit (e.g. the lockscreen authenticated). For a + // session-lock surface, unlock first — exiting without unlocking + // leaves the compositor's lock in place by design. + if data.app.requested_exit() + { + if let Some( lock ) = data.session_lock.take() + { + lock.unlock(); + let _ = conn.roundtrip(); + } + data.exit_requested = true; + } + // Seed `on_drag_move` with the long-press origin for any drag that // just started. Must run *after* `update()` so the app's drag state // has already been set up by the paired long-press message — the diff --git a/src/event_loop/surface.rs b/src/event_loop/surface.rs index f0f70f2..b7f64c4 100644 --- a/src/event_loop/surface.rs +++ b/src/event_loop/surface.rs @@ -17,6 +17,7 @@ use smithay_client_toolkit::reexports::client:: protocol::wl_surface::WlSurface, QueueHandle, }; +use smithay_client_toolkit::session_lock::SessionLockSurface; use std::collections::HashMap; use std::sync::Arc; @@ -70,6 +71,11 @@ pub( crate ) enum SurfaceKind /// to an anchor rect specified at creation time and may flip it /// (drop-up vs drop-down) when constrained. Popup( Popup ), + /// `ext-session-lock-v1` lock surface, created in `SessionLockHandler::locked`. + Lock( SessionLockSurface ), + /// Session lock requested; waiting for the compositor's `locked` event + /// before the lock surface exists. + PendingLock, } impl SurfaceKind @@ -81,9 +87,10 @@ impl SurfaceKind SurfaceKind::Layer( l ) => l.wl_surface(), SurfaceKind::Window( w ) => w.wl_surface(), SurfaceKind::Popup( p ) => p.wl_surface(), + SurfaceKind::Lock( s ) => s.wl_surface(), // Unreachable: draw_frame is only called when configured == true, // which only becomes true after on_configure, which requires a real surface. - SurfaceKind::Pending( .. ) => unreachable!( "surface not yet created" ), + SurfaceKind::Pending( .. ) | SurfaceKind::PendingLock => unreachable!( "surface not yet created" ), } } @@ -98,7 +105,8 @@ impl SurfaceKind SurfaceKind::Layer( l ) => Some( l.wl_surface() ), SurfaceKind::Window( w ) => Some( w.wl_surface() ), SurfaceKind::Popup( p ) => Some( p.wl_surface() ), - SurfaceKind::Pending( .. ) => None, + SurfaceKind::Lock( s ) => Some( s.wl_surface() ), + SurfaceKind::Pending( .. ) | SurfaceKind::PendingLock => None, } } diff --git a/src/widget/element.rs b/src/widget/element.rs index 7c834f1..74f8f70 100644 --- a/src/widget/element.rs +++ b/src/widget/element.rs @@ -189,7 +189,8 @@ impl Element match self { Element::Button( b ) => b.focusable, - Element::TextEdit( _ ) | Element::Slider( _ ) | Element::VSlider( _ ) => true, + Element::TextEdit( t ) => !t.read_only, + Element::Slider( _ ) | Element::VSlider( _ ) => true, Element::Toggle( _ ) | Element::Checkbox( _ ) | Element::Radio( _ ) | Element::ListItem( _ ) => true, Element::WindowButton( b ) => b.focusable, _ => false, @@ -198,7 +199,7 @@ impl Element pub fn is_text_input( &self ) -> bool { - matches!( self, Element::TextEdit( _ ) ) + matches!( self, Element::TextEdit( t ) if !t.read_only ) } /// Cursor shape to display while the pointer is over this widget. diff --git a/src/widget/list_item/mod.rs b/src/widget/list_item/mod.rs index a81bb94..14adb6e 100644 --- a/src/widget/list_item/mod.rs +++ b/src/widget/list_item/mod.rs @@ -81,7 +81,7 @@ impl ListItem /// Attach a leading icon. Pass the decoded RGBA buffer alongside /// the image's native width and height; the draw path scales it - /// down to [`theme::ICON_SIZE`] on the same row baseline as the + /// down to `theme::ICON_SIZE` on the same row baseline as the /// label. Symbolic icons should be pre-tinted by the caller (see /// [`crate::tint_symbolic`]). pub fn icon( mut self, rgba: Arc>, w: u32, h: u32 ) -> Self diff --git a/src/widget/text_edit/mod.rs b/src/widget/text_edit/mod.rs index cafe1a8..2e1366a 100644 --- a/src/widget/text_edit/mod.rs +++ b/src/widget/text_edit/mod.rs @@ -153,6 +153,9 @@ pub struct TextEdit /// has `secure( true )` and the explicit flag becomes redundant /// — the toggle controls the visibility from then on. pub password_toggle: Option<( bool, Msg )>, + /// When `true`, the field renders its box and value but takes no keyboard + /// focus and accepts no input — a read-only display styled as a text field. + pub read_only: bool, } impl TextEdit @@ -181,6 +184,7 @@ impl TextEdit font_size: theme::FONT_SIZE, select_on_focus: false, password_toggle: None, + read_only: false, } } @@ -356,6 +360,14 @@ impl TextEdit self } + /// Render the field read-only: shows the value styled as a field but + /// takes no focus and accepts no input. + pub fn read_only( mut self, on: bool ) -> Self + { + self.read_only = on; + self + } + /// Assign a stable identifier for focus management. pub fn id( mut self, id: WidgetId ) -> Self { @@ -487,6 +499,7 @@ impl TextEdit font_size: self.font_size, select_on_focus: self.select_on_focus, password_toggle: self.password_toggle.clone().map( |( v, m )| ( v, ( *f )( m ) ) ), + read_only: self.read_only, } } }