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