From 3259ef0d5305a6858ed467cbc06e3e99180a5acb Mon Sep 17 00:00:00 2001 From: Naman Agrawal Date: Thu, 28 Aug 2025 11:11:48 +0530 Subject: [PATCH] feat: add feature to create session lock surfaces. - Allow users to create session lock surfaces by passing an event `SessionLockEvent::Lock` and unlock them using `SessionLockEvent::Unlock`. - Other surfaces of the application should be destroyed before the session lock is aquired. The wayland client is only allowed to render to the lock surfaces while the lock is active. --- examples/session_lock.rs | 212 +++++++++++++++++++++++++++++++++++++++ src/lib.rs | 4 +- src/session_lock.rs | 156 ++++++++++++++++++++++++++++ 3 files changed, 371 insertions(+), 1 deletion(-) create mode 100644 examples/session_lock.rs create mode 100644 src/session_lock.rs diff --git a/examples/session_lock.rs b/examples/session_lock.rs new file mode 100644 index 0000000..7c5051b --- /dev/null +++ b/examples/session_lock.rs @@ -0,0 +1,212 @@ +use std::time::Duration; + +use bevy::{ + color::palettes::basic::*, + prelude::*, + window::{exit_on_all_closed, WindowCreated, WindowRef, WindowResolution}, + winit::WinitPlugin, +}; +use bevy_wayland::{ + layer_shell::LayerShellSettings, + session_lock::{SessionLockEvent, SessionLockWindow}, + WaylandPlugin, +}; +use smithay_client_toolkit::shell::wlr_layer::{Anchor, Layer}; + +const NORMAL_BUTTON: Color = Color::srgb(0.15, 0.15, 0.15); +const HOVERED_BUTTON: Color = Color::srgb(0.25, 0.25, 0.25); + +fn main() { + App::new() + .add_plugins(( + DefaultPlugins + .build() + .disable::() + .set(WindowPlugin { + primary_window: None, + ..Default::default() + }), + WaylandPlugin, + )) + .add_systems(Startup, setup) + .add_systems( + Update, + ( + button_system, + exit_on_esc, + setup_session_lock_windows, + exit_on_all_closed, + ), + ) + .run(); +} + +#[allow(clippy::type_complexity)] +fn button_system( + mut interaction_query: Query< + ( + &Interaction, + &mut BackgroundColor, + &mut BorderColor, + &Children, + ), + (Changed, With), + >, + mut text_query: Query<&mut Text>, + mut session_lock_event_writer: EventWriter, +) { + for (interaction, mut color, mut border_color, children) in &mut interaction_query { + let mut text = text_query.get_mut(children[0]).unwrap(); + match *interaction { + Interaction::Pressed => { + session_lock_event_writer.write(SessionLockEvent::Unlock); + } + Interaction::Hovered => { + **text = "Click to unlock".to_string(); + *color = HOVERED_BUTTON.into(); + border_color.0 = Color::WHITE; + } + Interaction::None => { + **text = "Click to unlock".to_string(); + *color = NORMAL_BUTTON.into(); + border_color.0 = Color::BLACK; + } + } + } +} + +#[derive(Component)] +struct LockButton; +#[derive(Component)] +struct UnlockButton; + +fn setup( + mut commands: Commands, + assets: Res, + windows: Query>, + + mut session_lock_event_writer: EventWriter, +) { + session_lock_event_writer.write(SessionLockEvent::Lock); + for entity in &windows { + commands.entity(entity).insert((LayerShellSettings { + anchor: Anchor::TOP | Anchor::LEFT, + layer: Layer::Bottom, + ..Default::default() + },)); + } + // ui camera + commands.spawn(Camera2d); + commands.spawn(lock_button(&assets)); +} + +#[derive(Component)] +struct ConfiguredWindow; +#[derive(Component)] +struct SessionLockCamera; + +fn setup_session_lock_windows( + mut commands: Commands, + asset_server: Res, + windows: Query<(Entity, &SessionLockWindow), Without>, +) { + for (entity, _) in &windows { + let camera = commands + .spawn(( + Camera2d, + Camera { + target: bevy::render::camera::RenderTarget::Window(WindowRef::Entity(entity)), + ..Default::default() + }, + SessionLockCamera, + )) + .id(); + commands.entity(entity).insert(ConfiguredWindow); + commands.spawn(unlock_button(&asset_server, camera)); + } +} + +fn exit_on_esc(keys: Res>) { + if keys.just_pressed(KeyCode::Escape) { + std::process::exit(0); + } +} + +fn lock_button(asset_server: &AssetServer) -> impl Bundle + use<> { + ( + Node { + width: Val::Percent(100.0), + height: Val::Percent(100.0), + align_items: AlignItems::Center, + justify_content: JustifyContent::Center, + ..default() + }, + children![( + Button, + LockButton, + Node { + width: Val::Px(250.0), + height: Val::Px(65.0), + border: UiRect::all(Val::Px(5.0)), + // horizontally center child text + justify_content: JustifyContent::Center, + // vertically center child text + align_items: AlignItems::Center, + ..default() + }, + BorderColor(Color::BLACK), + BorderRadius::MAX, + BackgroundColor(NORMAL_BUTTON), + children![( + Text::new("Button"), + TextFont { + font: asset_server.load("fonts/FiraSans-Bold.ttf"), + font_size: 33.0, + ..default() + }, + TextColor(Color::srgb(0.9, 0.9, 0.9)), + TextShadow::default(), + )] + )], + ) +} + +fn unlock_button(asset_server: &AssetServer, camera: Entity) -> impl Bundle + use<> { + ( + Node { + width: Val::Percent(100.0), + height: Val::Percent(100.0), + align_items: AlignItems::Center, + justify_content: JustifyContent::Center, + ..default() + }, + UiTargetCamera(camera), + children![( + Button, + UnlockButton, + Node { + width: Val::Px(250.0), + height: Val::Px(65.0), + border: UiRect::all(Val::Px(5.0)), + // horizontally center child text + justify_content: JustifyContent::Center, + // vertically center child text + align_items: AlignItems::Center, + ..default() + }, + BorderColor(Color::BLACK), + BorderRadius::MAX, + BackgroundColor(NORMAL_BUTTON), + children![( + Text::new("Button"), + TextFont { + font: asset_server.load("fonts/FiraSans-Bold.ttf"), + font_size: 33.0, + ..default() + }, + TextColor(Color::srgb(0.9, 0.9, 0.9)), + TextShadow::default(), + )] + )], + ) +} diff --git a/src/lib.rs b/src/lib.rs index 7b70f41..a5e20e3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,6 +16,7 @@ mod input_handler; pub mod input_region; pub mod layer_shell; mod output_handler; +pub mod session_lock; mod surface_handler; #[derive(Default)] @@ -42,9 +43,10 @@ impl Plugin for WaylandPlugin { app.add_plugins(( output_handler::OutputHandlerPlugin, - input_handler::InputHandlerPlugin, surface_handler::SurfaceHandlerPlugin, + input_handler::InputHandlerPlugin, layer_shell::LayerShellPlugin, + session_lock::SessionLockPlugin, input_region::InputRegionPlugin, )); app.set_runner(|app| runner(app, event_loop)); diff --git a/src/session_lock.rs b/src/session_lock.rs new file mode 100644 index 0000000..c57472a --- /dev/null +++ b/src/session_lock.rs @@ -0,0 +1,156 @@ +use bevy::{platform::collections::HashMap, prelude::*}; +use smithay_client_toolkit::{ + delegate_session_lock, + output::OutputState, + reexports::client::{globals::GlobalList, protocol::wl_output::WlOutput, QueueHandle}, + session_lock::{SessionLock, SessionLockHandler, SessionLockState, SessionLockSurface}, +}; + +use crate::{ + surface_handler::{create_windows, SurfaceConfigured, WaylandSurfaces}, + WaylandState, +}; + +#[derive(Default, Deref, DerefMut)] +struct SessionLockWindows(HashMap); +struct SessionLockWindowInternal { + _session_lock_surface: SessionLockSurface, +} + +#[derive(Component)] +pub struct SessionLockWindow; + +#[derive(Component)] +struct SessionLockUnconfiguredWindow { + output: WlOutput, +} +impl SessionLockUnconfiguredWindow { + pub fn new(output: WlOutput) -> Self { + Self { output } + } +} + +#[derive(Clone, Copy, Event)] +pub enum SessionLockEvent { + Lock, + Unlock, +} + +pub struct SessionLockPlugin; +impl Plugin for SessionLockPlugin { + fn build(&self, app: &mut App) { + let globals = app.world().non_send_resource::(); + let queue_handle = app.world().non_send_resource::>(); + let session_lock_state = SessionLockState::new(globals, queue_handle); + + app.insert_non_send_resource(session_lock_state); + app.insert_non_send_resource(SessionLockWindows::default()); + app.insert_non_send_resource(SessionLockWrapper::default()); + app.add_event::(); + app.add_systems( + PreUpdate, + ( + session_lock_event_handler.before(create_windows), + configure_lock_surfaces.after(create_windows), + ), + ); + } +} + +#[derive(Deref, DerefMut, Default)] +struct SessionLockWrapper(Option); +fn session_lock_event_handler( + mut commands: Commands, + mut session_lock_event_reader: EventReader, + session_lock_state: NonSend, + mut session_lock_wrapper: NonSendMut, + queue_handle: NonSend>, + output_state: NonSend, +) { + for session_lock_event in session_lock_event_reader.read() { + match session_lock_event { + SessionLockEvent::Lock => { + if session_lock_wrapper.is_some() { + error!("Lock was called even if it was already aquired"); + return; + } + let session_lock = session_lock_state + .lock(&queue_handle) + .expect("Unable to aquire session lock"); + let _ = session_lock_wrapper.insert(session_lock); + + for output in output_state.outputs() { + commands.spawn(( + Window::default(), + SessionLockUnconfiguredWindow::new(output), + )); + } + } + SessionLockEvent::Unlock => { + if let Some(session_lock) = &**session_lock_wrapper { + session_lock.unlock(); + } + } + } + } +} + +fn configure_lock_surfaces( + mut commands: Commands, + mut session_lock_windows: NonSendMut, + session_lock_wrapper: NonSend, + wayland_surfaces: NonSend, + qh: NonSend>, + unconfigured_windows: Query<(Entity, &SessionLockUnconfiguredWindow)>, +) { + if let Some(session_lock) = &**session_lock_wrapper { + for (entity, unconfigured_window) in &unconfigured_windows { + let window_wrapper = wayland_surfaces.get_window_wrapper(entity); + let surface = window_wrapper + .expect("tried to assign role before creating surface!") + .wl_surface(); + let _session_lock_surface = + session_lock.create_lock_surface(surface.clone(), &unconfigured_window.output, &qh); + + let session_lock_window = SessionLockWindowInternal { + _session_lock_surface, + }; + + session_lock_windows.insert(entity, session_lock_window); + commands + .entity(entity) + .insert(SurfaceConfigured) + .insert(SessionLockWindow) + .remove::(); + } + } +} + +impl SessionLockHandler for WaylandState { + fn locked( + &mut self, + _conn: &smithay_client_toolkit::reexports::client::Connection, + _qh: &smithay_client_toolkit::reexports::client::QueueHandle, + _session_lock: smithay_client_toolkit::session_lock::SessionLock, + ) { + } + + fn finished( + &mut self, + _conn: &smithay_client_toolkit::reexports::client::Connection, + _qh: &smithay_client_toolkit::reexports::client::QueueHandle, + _session_lock: smithay_client_toolkit::session_lock::SessionLock, + ) { + } + + fn configure( + &mut self, + _conn: &smithay_client_toolkit::reexports::client::Connection, + _qh: &smithay_client_toolkit::reexports::client::QueueHandle, + _surface: smithay_client_toolkit::session_lock::SessionLockSurface, + _configure: smithay_client_toolkit::session_lock::SessionLockSurfaceConfigure, + _serial: u32, + ) { + } +} +delegate_session_lock!(WaylandState);