From 91455060c725c4a9c2f8e64280dd073504f4df7a Mon Sep 17 00:00:00 2001 From: drendog Date: Sun, 28 Dec 2025 22:01:13 +0100 Subject: [PATCH] feat: add session lock support --- README.md | 7 +- .../wayland/event_handling/app_dispatcher.rs | 283 +++++-- .../event_handling/event_dispatcher.rs | 59 +- .../adapters/src/wayland/globals/context.rs | 11 + crates/adapters/src/wayland/mod.rs | 1 + crates/adapters/src/wayland/ops.rs | 20 + .../src/wayland/session_lock/lock_context.rs | 75 ++ .../src/wayland/session_lock/lock_manager.rs | 763 ++++++++++++++++++ .../src/wayland/session_lock/lock_surface.rs | 118 +++ .../adapters/src/wayland/session_lock/mod.rs | 3 + crates/adapters/src/wayland/shell_adapter.rs | 57 +- .../src/wayland/surfaces/app_state.rs | 230 +++++- .../src/wayland/surfaces/keyboard_state.rs | 46 ++ crates/adapters/src/wayland/surfaces/mod.rs | 1 + .../src/wayland/surfaces/pointer_utils.rs | 12 + crates/composition/src/lib.rs | 17 +- crates/composition/src/session_lock.rs | 190 +++++ crates/composition/src/shell.rs | 55 +- crates/composition/src/system.rs | 12 + crates/domain/src/prelude.rs | 2 + .../domain/src/value_objects/lock_config.rs | 40 + crates/domain/src/value_objects/lock_state.rs | 24 + crates/domain/src/value_objects/mod.rs | 2 + 23 files changed, 1893 insertions(+), 135 deletions(-) create mode 100644 crates/adapters/src/wayland/session_lock/lock_context.rs create mode 100644 crates/adapters/src/wayland/session_lock/lock_manager.rs create mode 100644 crates/adapters/src/wayland/session_lock/lock_surface.rs create mode 100644 crates/adapters/src/wayland/session_lock/mod.rs create mode 100644 crates/adapters/src/wayland/surfaces/pointer_utils.rs create mode 100644 crates/composition/src/session_lock.rs create mode 100644 crates/domain/src/value_objects/lock_config.rs create mode 100644 crates/domain/src/value_objects/lock_state.rs diff --git a/README.md b/README.md index ff1110c..9bbbd3c 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@

🦌

-

"A cute layer of abstraction where Slint UIs grow antlers and become cute Wayland shells."

+

"A cute library where Slint UIs grow antlers and become cute Wayland shells."

Main repo | Mirror | Temp mirror (github)

@@ -19,17 +19,18 @@ Oh deer! 🦌 You've stumbled upon `layer-shika`, a Rust library providing Wayla - **Slint Integration**: Runtime `.slint` file compilation via slint-interpreter or compile-time code generation. Support via pre-compiled is planned. - **Multi-Surface Support**: Create multiple independent layer shell windows, each with its own configuration and lifecycle - **Flexible Configuration**: Both fluent builder API and declarative configuration support -- **Comprehensive Popup System**: Full xdg-popup protocol implementation with multiple positioning modes, grab support, and content-based sizing (rework in progress) +- **Comprehensive Popup System**: Full `xdg-popup` protocol implementation with multiple positioning modes, grab support, and content-based sizing (rework in progress) - **Multi-Output Support**: Per-monitor component instances with flexible output policies (primary only, all outputs, specific outputs) - **Event Loop Integration**: Custom event sources (timers, channels, file descriptors) via calloop integration - **Clean-like Architecture**: Organized as a Cargo workspace with clear separation of concerns (domain, adapters, composition) - **HiDPI Support**: Configurable scale factors for high-resolution displays +- **Session Lock Support**: `ext-session-lock-v1` protocol integration for lock screens ## Architecture layer-shika is organized as a **Cargo workspace** with three crates: -- **domain** ([crates/domain/](crates/domain/)): Core domain models, value objects, and port trait definitions. No framework dependencies. +- **domain** ([crates/domain/](crates/domain/)): Core domain models, value objects, and port trait definitions. No external dependencies. - **adapters** ([crates/adapters/](crates/adapters/)): Concrete implementations for Wayland (smithay-client-toolkit), rendering (femtovg + EGL), and platform integration. - **composition** ([crates/composition/](crates/composition/)): Public API layer providing Shell-based API, builder patterns, and system integration. diff --git a/crates/adapters/src/wayland/event_handling/app_dispatcher.rs b/crates/adapters/src/wayland/event_handling/app_dispatcher.rs index d86d1d1..6d55935 100644 --- a/crates/adapters/src/wayland/event_handling/app_dispatcher.rs +++ b/crates/adapters/src/wayland/event_handling/app_dispatcher.rs @@ -22,6 +22,11 @@ use wayland_client::{ wl_surface::WlSurface, }, }; +use wayland_protocols::ext::session_lock::v1::client::{ + ext_session_lock_manager_v1::ExtSessionLockManagerV1, + ext_session_lock_surface_v1::{self, ExtSessionLockSurfaceV1}, + ext_session_lock_v1::{self, ExtSessionLockV1}, +}; use wayland_protocols::wp::fractional_scale::v1::client::{ wp_fractional_scale_manager_v1::WpFractionalScaleManagerV1, wp_fractional_scale_v1::{self, WpFractionalScaleV1}, @@ -188,6 +193,137 @@ impl Dispatch for AppState { } } +fn handle_pointer_enter_event( + state: &mut AppState, + serial: u32, + surface: &WlSurface, + surface_x: f64, + surface_y: f64, +) { + let surface_id = surface.id(); + + if let Some(manager) = state.lock_manager_mut() { + if manager.handle_pointer_enter(serial, surface, surface_x, surface_y) { + state.set_active_surface_key(None); + return; + } + } + + if let Some(key) = state.get_key_by_surface(&surface_id).cloned() { + if let Some(layer_surface) = state.get_surface_by_key_mut(&key) { + layer_surface.handle_pointer_enter(serial, surface, surface_x, surface_y); + } + state.set_active_surface_key(Some(key)); + return; + } + + if let Some(key) = state.get_key_by_popup(&surface_id).cloned() { + if let Some(layer_surface) = state.get_surface_by_key_mut(&key) { + layer_surface.handle_pointer_enter(serial, surface, surface_x, surface_y); + } + state.set_active_surface_key(Some(key)); + } +} + +fn handle_pointer_motion_event(state: &mut AppState, surface_x: f64, surface_y: f64) { + if let Some(manager) = state.lock_manager_mut() { + if manager.handle_pointer_motion(surface_x, surface_y) { + return; + } + } + + if let Some(surface) = state.active_surface_mut() { + surface.handle_pointer_motion(surface_x, surface_y); + } +} + +fn handle_pointer_leave_event(state: &mut AppState) { + if let Some(manager) = state.lock_manager_mut() { + if manager.handle_pointer_leave() { + state.set_active_surface_key(None); + return; + } + } + + if let Some(surface) = state.active_surface_mut() { + surface.handle_pointer_leave(); + } + state.set_active_surface_key(None); +} + +fn handle_pointer_button_event( + state: &mut AppState, + serial: u32, + button: u32, + button_state: WEnum, +) { + if let Some(manager) = state.lock_manager_mut() { + if manager.handle_pointer_button(serial, button, button_state) { + return; + } + } + + if let Some(surface) = state.active_surface_mut() { + surface.handle_pointer_button(serial, button, button_state); + } +} + +fn handle_pointer_axis_source_event(state: &mut AppState, axis_source: wl_pointer::AxisSource) { + if let Some(manager) = state.lock_manager_mut() { + if manager.handle_axis_source(axis_source) { + return; + } + } + if let Some(surface) = state.active_surface_mut() { + surface.handle_axis_source(axis_source); + } +} + +fn handle_pointer_axis_event(state: &mut AppState, time: u32, axis: wl_pointer::Axis, value: f64) { + if let Some(manager) = state.lock_manager_mut() { + if manager.handle_axis(axis, value) { + return; + } + } + if let Some(surface) = state.active_surface_mut() { + surface.handle_axis(time, axis, value); + } +} + +fn handle_pointer_axis_discrete_event(state: &mut AppState, axis: wl_pointer::Axis, discrete: i32) { + if let Some(manager) = state.lock_manager_mut() { + if manager.handle_axis_discrete(axis, discrete) { + return; + } + } + if let Some(surface) = state.active_surface_mut() { + surface.handle_axis_discrete(axis, discrete); + } +} + +fn handle_pointer_axis_stop_event(state: &mut AppState, time: u32, axis: wl_pointer::Axis) { + if let Some(manager) = state.lock_manager_mut() { + if manager.handle_axis_stop(axis) { + return; + } + } + if let Some(surface) = state.active_surface_mut() { + surface.handle_axis_stop(time, axis); + } +} + +fn handle_pointer_frame_event(state: &mut AppState) { + if let Some(manager) = state.lock_manager_mut() { + if manager.handle_pointer_frame() { + return; + } + } + + if let Some(surface) = state.active_surface_mut() { + surface.handle_pointer_frame(); + } +} + impl Dispatch for AppState { fn event( state: &mut Self, @@ -203,76 +339,36 @@ impl Dispatch for AppState { surface, surface_x, surface_y, - } => { - let surface_id = surface.id(); - - if let Some(key) = state.get_key_by_surface(&surface_id).cloned() { - if let Some(layer_surface) = state.get_surface_by_key_mut(&key) { - layer_surface.handle_pointer_enter(serial, &surface, surface_x, surface_y); - } - state.set_active_surface_key(Some(key)); - } else if let Some(key) = state.get_key_by_popup(&surface_id).cloned() { - if let Some(layer_surface) = state.get_surface_by_key_mut(&key) { - layer_surface.handle_pointer_enter(serial, &surface, surface_x, surface_y); - } - state.set_active_surface_key(Some(key)); - } - } - + } => handle_pointer_enter_event(state, serial, &surface, surface_x, surface_y), wl_pointer::Event::Motion { surface_x, surface_y, .. - } => { - if let Some(surface) = state.active_surface_mut() { - surface.handle_pointer_motion(surface_x, surface_y); - } - } - - wl_pointer::Event::Leave { .. } => { - if let Some(surface) = state.active_surface_mut() { - surface.handle_pointer_leave(); - } - state.set_active_surface_key(None); - } - + } => handle_pointer_motion_event(state, surface_x, surface_y), + wl_pointer::Event::Leave { .. } => handle_pointer_leave_event(state), wl_pointer::Event::Button { serial, button, state: button_state, .. - } => { - if let Some(surface) = state.active_surface_mut() { - surface.handle_pointer_button(serial, button, button_state); - } - } - wl_pointer::Event::AxisSource { axis_source } => { - if let (Some(surface), WEnum::Value(axis_source)) = - (state.active_surface_mut(), axis_source) - { - surface.handle_axis_source(axis_source); - } - } - wl_pointer::Event::Axis { time, axis, value } => { - if let (Some(surface), WEnum::Value(axis)) = (state.active_surface_mut(), axis) { - surface.handle_axis(time, axis, value); - } - } - wl_pointer::Event::AxisDiscrete { axis, discrete } => { - if let (Some(surface), WEnum::Value(axis)) = (state.active_surface_mut(), axis) { - surface.handle_axis_discrete(axis, discrete); - } - } - wl_pointer::Event::AxisStop { time, axis } => { - if let (Some(surface), WEnum::Value(axis)) = (state.active_surface_mut(), axis) { - surface.handle_axis_stop(time, axis); - } - } - wl_pointer::Event::Frame => { - if let Some(surface) = state.active_surface_mut() { - surface.handle_pointer_frame(); - } - } + } => handle_pointer_button_event(state, serial, button, button_state), + wl_pointer::Event::AxisSource { + axis_source: WEnum::Value(axis_source), + } => handle_pointer_axis_source_event(state, axis_source), + wl_pointer::Event::Axis { + time, + axis: WEnum::Value(axis), + value, + } => handle_pointer_axis_event(state, time, axis, value), + wl_pointer::Event::AxisDiscrete { + axis: WEnum::Value(axis), + discrete, + } => handle_pointer_axis_discrete_event(state, axis, discrete), + wl_pointer::Event::AxisStop { + time, + axis: WEnum::Value(axis), + } => handle_pointer_axis_stop_event(state, time, axis), + wl_pointer::Event::Frame => handle_pointer_frame_event(state), _ => {} } } @@ -346,6 +442,10 @@ impl Dispatch for AppState { for surface in state.all_outputs_mut() { surface.handle_fractional_scale(proxy, scale); } + + if let Some(manager) = state.lock_manager_mut() { + manager.handle_fractional_scale(&proxy.id(), scale); + } } } } @@ -471,6 +571,7 @@ impl Dispatch for AppState { let output = registry.bind::(name, 4.min(version), qhandle, ()); let output_id = output.id(); + let output_for_lock = output.clone(); if let Some(manager) = state.output_manager() { let mut manager_ref = manager.borrow_mut(); @@ -478,6 +579,11 @@ impl Dispatch for AppState { info!("Registered hot-plugged output with handle {handle:?}"); state.register_registry_name(name, output_id); + if let Err(err) = + state.handle_output_added_for_lock(&output_for_lock, qhandle) + { + info!("Failed to add session lock surface for output: {err}"); + } } else { info!("No output manager available yet (startup initialization)"); } @@ -489,6 +595,8 @@ impl Dispatch for AppState { if let Some(output_id) = state.unregister_registry_name(name) { info!("Output with registry name {name} removed, cleaning up..."); + state.handle_output_removed_for_lock(&output_id); + if let Some(manager) = state.output_manager() { let mut manager_ref = manager.borrow_mut(); manager_ref.remove_output(&output_id, state); @@ -500,6 +608,58 @@ impl Dispatch for AppState { } } +impl Dispatch for AppState { + fn event( + state: &mut Self, + _proxy: &ExtSessionLockV1, + event: ext_session_lock_v1::Event, + _data: &(), + _conn: &Connection, + _queue_handle: &QueueHandle, + ) { + match event { + ext_session_lock_v1::Event::Locked => { + if let Some(manager) = state.lock_manager_mut() { + manager.handle_locked(); + } + } + ext_session_lock_v1::Event::Finished => { + if let Some(manager) = state.lock_manager_mut() { + manager.handle_finished(); + } + state.clear_lock_manager(); + } + _ => {} + } + } +} + +impl Dispatch for AppState { + fn event( + state: &mut Self, + lock_surface: &ExtSessionLockSurfaceV1, + event: ext_session_lock_surface_v1::Event, + _data: &(), + _conn: &Connection, + _queue_handle: &QueueHandle, + ) { + if let ext_session_lock_surface_v1::Event::Configure { + serial, + width, + height, + } = event + { + let lock_surface_id = lock_surface.id(); + if let Some(manager) = state.lock_manager_mut() { + if let Err(err) = manager.handle_configure(&lock_surface_id, serial, width, height) + { + info!("Failed to configure session lock surface: {err}"); + } + } + } + } +} + macro_rules! impl_empty_dispatch_app { ($(($t:ty, $u:ty)),+) => { $( @@ -523,6 +683,7 @@ impl_empty_dispatch_app!( (WlCompositor, ()), (WlSurface, ()), (ZwlrLayerShellV1, ()), + (ExtSessionLockManagerV1, ()), (WlSeat, ()), (WpFractionalScaleManagerV1, ()), (WpViewporter, ()), diff --git a/crates/adapters/src/wayland/event_handling/event_dispatcher.rs b/crates/adapters/src/wayland/event_handling/event_dispatcher.rs index 60817e7..042c829 100644 --- a/crates/adapters/src/wayland/event_handling/event_dispatcher.rs +++ b/crates/adapters/src/wayland/event_handling/event_dispatcher.rs @@ -1,9 +1,10 @@ -use crate::wayland::surfaces::keyboard_state::KeyboardState; +use crate::wayland::surfaces::keyboard_state::{KeyboardState, keysym_to_text}; +use crate::wayland::surfaces::pointer_utils::wayland_button_to_slint; use crate::wayland::surfaces::surface_state::SurfaceState; use log::info; use slint::{ PhysicalSize, - platform::{Key, PointerEventButton, WindowEvent}, + platform::WindowEvent, }; use slint::SharedString; use smithay_client_toolkit::reexports::protocols_wlr::layer_shell::v1::client::{ @@ -130,14 +131,7 @@ impl SurfaceState { ) { self.set_last_pointer_serial(serial); let position = self.current_pointer_position(); - let slint_button = match button { - 0x110 => PointerEventButton::Left, - 0x111 => PointerEventButton::Right, - 0x112 => PointerEventButton::Middle, - 0x115 => PointerEventButton::Forward, - 0x116 => PointerEventButton::Back, - _ => PointerEventButton::Other, - }; + let slint_button = wayland_button_to_slint(button); let event = match button_state { WEnum::Value(wl_pointer::ButtonState::Pressed) => WindowEvent::PointerPressed { button: slint_button, @@ -282,48 +276,3 @@ impl SurfaceState { xdg_wm_base.pong(serial); } } - -fn keysym_to_text(keysym: xkb::Keysym) -> Option { - let key = match keysym.raw() { - xkb::keysyms::KEY_Return | xkb::keysyms::KEY_KP_Enter => Key::Return, - xkb::keysyms::KEY_BackSpace => Key::Backspace, - xkb::keysyms::KEY_Tab => Key::Tab, - xkb::keysyms::KEY_BackTab => Key::Backtab, - xkb::keysyms::KEY_Escape => Key::Escape, - xkb::keysyms::KEY_Delete => Key::Delete, - xkb::keysyms::KEY_Insert => Key::Insert, - xkb::keysyms::KEY_Home => Key::Home, - xkb::keysyms::KEY_End => Key::End, - xkb::keysyms::KEY_Page_Up => Key::PageUp, - xkb::keysyms::KEY_Page_Down => Key::PageDown, - xkb::keysyms::KEY_Left => Key::LeftArrow, - xkb::keysyms::KEY_Right => Key::RightArrow, - xkb::keysyms::KEY_Up => Key::UpArrow, - xkb::keysyms::KEY_Down => Key::DownArrow, - xkb::keysyms::KEY_space => Key::Space, - xkb::keysyms::KEY_Shift_L => Key::Shift, - xkb::keysyms::KEY_Shift_R => Key::ShiftR, - xkb::keysyms::KEY_Control_L => Key::Control, - xkb::keysyms::KEY_Control_R => Key::ControlR, - xkb::keysyms::KEY_Alt_L | xkb::keysyms::KEY_Alt_R => Key::Alt, - xkb::keysyms::KEY_Mode_switch => Key::AltGr, - xkb::keysyms::KEY_Meta_L => Key::Meta, - xkb::keysyms::KEY_Meta_R => Key::MetaR, - xkb::keysyms::KEY_Caps_Lock => Key::CapsLock, - xkb::keysyms::KEY_F1 => Key::F1, - xkb::keysyms::KEY_F2 => Key::F2, - xkb::keysyms::KEY_F3 => Key::F3, - xkb::keysyms::KEY_F4 => Key::F4, - xkb::keysyms::KEY_F5 => Key::F5, - xkb::keysyms::KEY_F6 => Key::F6, - xkb::keysyms::KEY_F7 => Key::F7, - xkb::keysyms::KEY_F8 => Key::F8, - xkb::keysyms::KEY_F9 => Key::F9, - xkb::keysyms::KEY_F10 => Key::F10, - xkb::keysyms::KEY_F11 => Key::F11, - xkb::keysyms::KEY_F12 => Key::F12, - _ => return None, - }; - - Some(SharedString::from(key)) -} diff --git a/crates/adapters/src/wayland/globals/context.rs b/crates/adapters/src/wayland/globals/context.rs index 42055c3..5687e9a 100644 --- a/crates/adapters/src/wayland/globals/context.rs +++ b/crates/adapters/src/wayland/globals/context.rs @@ -13,6 +13,7 @@ use wayland_client::{ use wayland_protocols::wp::fractional_scale::v1::client::wp_fractional_scale_manager_v1::WpFractionalScaleManagerV1; use wayland_protocols::wp::viewporter::client::wp_viewporter::WpViewporter; use wayland_protocols::xdg::shell::client::xdg_wm_base::XdgWmBase; +use wayland_protocols::ext::session_lock::v1::client::ext_session_lock_manager_v1::ExtSessionLockManagerV1; use crate::wayland::surfaces::app_state::AppState; @@ -22,6 +23,7 @@ pub struct GlobalContext { pub layer_shell: ZwlrLayerShellV1, pub seat: WlSeat, pub xdg_wm_base: Option, + pub session_lock_manager: Option, pub fractional_scale_manager: Option, pub viewporter: Option, pub render_context_manager: Rc, @@ -85,6 +87,10 @@ impl GlobalContext { .bind::(queue_handle, 1..=6, ()) .ok(); + let session_lock_manager = global_list + .bind::(queue_handle, 1..=1, ()) + .ok(); + let fractional_scale_manager = global_list .bind::(queue_handle, 1..=1, ()) .ok(); @@ -97,6 +103,10 @@ impl GlobalContext { info!("xdg-shell protocol not available, popup support disabled"); } + if session_lock_manager.is_none() { + info!("ext-session-lock protocol not available, session lock disabled"); + } + if fractional_scale_manager.is_none() { info!("Fractional scale protocol not available, using integer scaling"); } @@ -113,6 +123,7 @@ impl GlobalContext { layer_shell, seat, xdg_wm_base, + session_lock_manager, fractional_scale_manager, viewporter, render_context_manager, diff --git a/crates/adapters/src/wayland/mod.rs b/crates/adapters/src/wayland/mod.rs index 5eeab7f..a557459 100644 --- a/crates/adapters/src/wayland/mod.rs +++ b/crates/adapters/src/wayland/mod.rs @@ -4,5 +4,6 @@ pub(crate) mod globals; pub(crate) mod managed_proxies; pub mod ops; pub(crate) mod outputs; +pub(crate) mod session_lock; pub(crate) mod shell_adapter; pub(crate) mod surfaces; diff --git a/crates/adapters/src/wayland/ops.rs b/crates/adapters/src/wayland/ops.rs index 22b10d3..33524da 100644 --- a/crates/adapters/src/wayland/ops.rs +++ b/crates/adapters/src/wayland/ops.rs @@ -1,9 +1,15 @@ use crate::errors::Result; use crate::wayland::config::ShellSurfaceConfig; use crate::wayland::surfaces::app_state::AppState; +use layer_shika_domain::value_objects::lock_config::LockConfig; +use layer_shika_domain::value_objects::lock_state::LockState; +use slint_interpreter::Value; use layer_shika_domain::value_objects::output_handle::OutputHandle; use slint_interpreter::ComponentInstance; use smithay_client_toolkit::reexports::calloop::LoopHandle; +use std::rc::Rc; + +type SessionLockCallback = Rc Value>; pub trait WaylandSystemOps { fn run(&mut self) -> Result<()>; @@ -12,6 +18,20 @@ pub trait WaylandSystemOps { fn despawn_surface(&mut self, name: &str) -> Result<()>; + fn activate_session_lock(&mut self, component_name: &str, config: LockConfig) -> Result<()>; + + fn deactivate_session_lock(&mut self) -> Result<()>; + + fn is_session_lock_available(&self) -> bool; + + fn session_lock_state(&self) -> Option; + + fn register_session_lock_callback( + &mut self, + callback_name: &str, + handler: SessionLockCallback, + ); + fn app_state(&self) -> &AppState; fn app_state_mut(&mut self) -> &mut AppState; diff --git a/crates/adapters/src/wayland/session_lock/lock_context.rs b/crates/adapters/src/wayland/session_lock/lock_context.rs new file mode 100644 index 0000000..5a8cb00 --- /dev/null +++ b/crates/adapters/src/wayland/session_lock/lock_context.rs @@ -0,0 +1,75 @@ +use crate::rendering::egl::context_factory::RenderContextFactory; +use crate::wayland::surfaces::app_state::AppState; +use std::rc::Rc; +use wayland_client::{ + QueueHandle, + protocol::{wl_compositor::WlCompositor, wl_output::WlOutput, wl_seat::WlSeat}, +}; +use wayland_protocols::ext::session_lock::v1::client::ext_session_lock_manager_v1::ExtSessionLockManagerV1; +use wayland_protocols::ext::session_lock::v1::client::ext_session_lock_v1::ExtSessionLockV1; +use wayland_protocols::wp::fractional_scale::v1::client::wp_fractional_scale_manager_v1::WpFractionalScaleManagerV1; +use wayland_protocols::wp::viewporter::client::wp_viewporter::WpViewporter; + +#[derive(Clone)] +pub struct SessionLockContext { + compositor: WlCompositor, + lock_manager: ExtSessionLockManagerV1, + seat: WlSeat, + fractional_scale_manager: Option, + viewporter: Option, + render_factory: Rc, +} + +impl SessionLockContext { + #[must_use] + pub fn new( + compositor: WlCompositor, + lock_manager: ExtSessionLockManagerV1, + seat: WlSeat, + fractional_scale_manager: Option, + viewporter: Option, + render_factory: Rc, + ) -> Self { + Self { + compositor, + lock_manager, + seat, + fractional_scale_manager, + viewporter, + render_factory, + } + } + + pub const fn compositor(&self) -> &WlCompositor { + &self.compositor + } + + pub const fn lock_manager(&self) -> &ExtSessionLockManagerV1 { + &self.lock_manager + } + + pub const fn seat(&self) -> &WlSeat { + &self.seat + } + + pub const fn fractional_scale_manager(&self) -> Option<&WpFractionalScaleManagerV1> { + self.fractional_scale_manager.as_ref() + } + + pub const fn viewporter(&self) -> Option<&WpViewporter> { + self.viewporter.as_ref() + } + + pub const fn render_factory(&self) -> &Rc { + &self.render_factory + } +} + +pub struct LockSurfaceParams<'a> { + pub compositor: &'a WlCompositor, + pub output: &'a WlOutput, + pub session_lock: &'a ExtSessionLockV1, + pub fractional_scale_manager: Option<&'a WpFractionalScaleManagerV1>, + pub viewporter: Option<&'a WpViewporter>, + pub queue_handle: &'a QueueHandle, +} diff --git a/crates/adapters/src/wayland/session_lock/lock_manager.rs b/crates/adapters/src/wayland/session_lock/lock_manager.rs new file mode 100644 index 0000000..1e77540 --- /dev/null +++ b/crates/adapters/src/wayland/session_lock/lock_manager.rs @@ -0,0 +1,763 @@ +use crate::errors::{LayerShikaError, Result}; +use crate::rendering::femtovg::main_window::FemtoVGWindow; +use crate::rendering::femtovg::renderable_window::RenderableWindow; +use crate::rendering::slint_integration::platform::CustomSlintPlatform; +use crate::wayland::session_lock::lock_context::{LockSurfaceParams, SessionLockContext}; +use crate::wayland::session_lock::lock_surface::LockSurface; +use crate::wayland::surfaces::app_state::AppState; +use crate::wayland::surfaces::component_state::ComponentState; +use crate::wayland::surfaces::display_metrics::DisplayMetrics; +use crate::wayland::surfaces::keyboard_state::{KeyboardState, keysym_to_text}; +use crate::wayland::surfaces::pointer_utils::wayland_button_to_slint; +use layer_shika_domain::surface_dimensions::SurfaceDimensions; +use layer_shika_domain::value_objects::lock_config::LockConfig; +use layer_shika_domain::value_objects::lock_state::LockState; +use log::info; +use slint::{ + LogicalPosition, LogicalSize, SharedString, WindowPosition, WindowSize, + platform::{ + WindowAdapter, WindowEvent, femtovg_renderer::FemtoVGRenderer, + }, +}; +use slint_interpreter::{CompilationResult, ComponentDefinition, ComponentInstance, Value}; +use std::collections::HashMap; +use std::rc::Rc; +use wayland_client::{ + Proxy, QueueHandle, WEnum, + backend::ObjectId, + protocol::{wl_keyboard, wl_output::WlOutput, wl_pointer, wl_surface::WlSurface}, +}; +use wayland_protocols::ext::session_lock::v1::client::ext_session_lock_v1::ExtSessionLockV1; +use xkbcommon::xkb; + +type LockCallbackHandler = Rc Value>; + +#[derive(Clone)] +pub(crate) struct LockCallback { + name: String, + handler: LockCallbackHandler, +} + +impl LockCallback { + pub fn new(name: impl Into, handler: LockCallbackHandler) -> Self { + Self { + name: name.into(), + handler, + } + } + + pub fn apply_to(&self, component: &ComponentInstance) -> Result<()> { + let handler = Rc::clone(&self.handler); + component + .set_callback(&self.name, move |args| handler(args)) + .map_err(|e| LayerShikaError::InvalidInput { + message: format!("Failed to register callback '{}': {e}", self.name), + }) + } +} + +struct ActiveLockSurface { + surface: LockSurface, + window: Rc, + component: Option, + scale_factor: f32, + has_fractional_scale: bool, +} + +struct LockConfigureContext { + scale_factor: f32, + component_definition: ComponentDefinition, + compilation_result: Option>, + platform: Rc, + callbacks: Vec, +} + +impl ActiveLockSurface { + fn new(surface: LockSurface, window: Rc) -> Self { + Self { + has_fractional_scale: surface.fractional_scale().is_some(), + surface, + window, + component: None, + scale_factor: 1.0, + } + } + + fn handle_configure( + &mut self, + serial: u32, + width: u32, + height: u32, + context: &LockConfigureContext, + ) -> Result<()> { + self.surface.handle_configure(serial, width, height); + self.scale_factor = context.scale_factor; + let dimensions = match SurfaceDimensions::calculate(width, height, context.scale_factor) { + Ok(dimensions) => dimensions, + Err(err) => { + info!("Failed to calculate lock surface dimensions: {err}"); + return Ok(()); + } + }; + let scaling_mode = self.scaling_mode(); + info!( + "Lock surface dimensions: logical {}x{}, physical {}x{}, scale {}, mode {:?}", + dimensions.logical_width(), + dimensions.logical_height(), + dimensions.physical_width(), + dimensions.physical_height(), + context.scale_factor, + scaling_mode + ); + self.configure_window(&dimensions, scaling_mode, context.scale_factor); + self.configure_surface(&dimensions, scaling_mode); + + if self.component.is_none() { + context.platform.add_window(Rc::clone(&self.window)); + let component = ComponentState::new( + context.component_definition.clone(), + context.compilation_result.clone(), + &self.window, + )?; + self.window + .window() + .dispatch_event(WindowEvent::WindowActiveChanged(true)); + for callback in &context.callbacks { + if let Err(err) = callback.apply_to(component.component_instance()) { + info!( + "Failed to register lock callback '{}': {err}", + callback.name + ); + } else { + info!("Registered lock callback '{}'", callback.name); + } + } + self.component = Some(component); + } + + RenderableWindow::request_redraw(self.window.as_ref()); + Ok(()) + } + + fn render_frame_if_dirty(&self) -> Result<()> { + self.window.render_frame_if_dirty() + } + + fn handle_fractional_scale(&mut self, scale_120ths: u32) { + let scale_factor = DisplayMetrics::scale_factor_from_120ths(scale_120ths); + self.scale_factor = scale_factor; + if self.surface.width() == 0 || self.surface.height() == 0 { + return; + } + let Ok(dimensions) = + SurfaceDimensions::calculate(self.surface.width(), self.surface.height(), scale_factor) + else { + return; + }; + let scaling_mode = self.scaling_mode(); + self.configure_window(&dimensions, scaling_mode, scale_factor); + self.configure_surface(&dimensions, scaling_mode); + RenderableWindow::request_redraw(self.window.as_ref()); + } + + fn apply_callback(&self, callback: &LockCallback) { + if let Some(component) = self.component.as_ref() { + if let Err(err) = callback.apply_to(component.component_instance()) { + info!( + "Failed to register lock callback '{}': {err}", + callback.name + ); + } + } + } + + fn scaling_mode(&self) -> LockScalingMode { + if self.surface.has_fractional_scale() && self.surface.has_viewport() { + LockScalingMode::FractionalWithViewport + } else if self.surface.has_fractional_scale() { + LockScalingMode::FractionalOnly + } else { + LockScalingMode::Integer + } + } + + #[allow(clippy::cast_precision_loss)] + fn configure_window( + &self, + dimensions: &SurfaceDimensions, + mode: LockScalingMode, + scale_factor: f32, + ) { + match mode { + LockScalingMode::FractionalWithViewport | LockScalingMode::FractionalOnly => { + RenderableWindow::set_scale_factor(self.window.as_ref(), scale_factor); + self.window.set_size(WindowSize::Logical(LogicalSize::new( + dimensions.logical_width() as f32, + dimensions.logical_height() as f32, + ))); + } + LockScalingMode::Integer => { + RenderableWindow::set_scale_factor(self.window.as_ref(), scale_factor); + self.window + .set_size(WindowSize::Physical(slint::PhysicalSize::new( + dimensions.physical_width(), + dimensions.physical_height(), + ))); + } + } + } + + fn configure_surface(&self, dimensions: &SurfaceDimensions, mode: LockScalingMode) { + match mode { + LockScalingMode::FractionalWithViewport => { + self.surface.configure_fractional_viewport( + dimensions.logical_width(), + dimensions.logical_height(), + ); + } + LockScalingMode::FractionalOnly | LockScalingMode::Integer => { + self.surface + .configure_buffer_scale(dimensions.buffer_scale()); + } + } + } + + #[allow(clippy::cast_possible_truncation)] + fn to_logical_position(&self, surface_x: f64, surface_y: f64) -> LogicalPosition { + if self.has_fractional_scale { + let x = surface_x as f32; + let y = surface_y as f32; + LogicalPosition::new(x, y) + } else { + let x = (surface_x / f64::from(self.scale_factor)) as f32; + let y = (surface_y / f64::from(self.scale_factor)) as f32; + LogicalPosition::new(x, y) + } + } + + fn dispatch_event(&self, event: WindowEvent) { + self.window.window().dispatch_event(event); + } + + fn window_rc(&self) -> Rc { + Rc::clone(&self.window) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum LockScalingMode { + FractionalWithViewport, + FractionalOnly, + Integer, +} + +pub struct SessionLockManager { + context: Rc, + session_lock: Option, + lock_surfaces: HashMap, + state: LockState, + config: LockConfig, + component_definition: ComponentDefinition, + compilation_result: Option>, + platform: Rc, + callbacks: Vec, + active_pointer_surface_id: Option, + keyboard_focus_surface_id: Option, + current_pointer_position: LogicalPosition, + accumulated_axis_x: f32, + accumulated_axis_y: f32, +} + +impl SessionLockManager { + #[must_use] + pub fn new( + context: Rc, + component_definition: ComponentDefinition, + compilation_result: Option>, + platform: Rc, + config: LockConfig, + ) -> Self { + Self { + context, + session_lock: None, + lock_surfaces: HashMap::new(), + state: LockState::Inactive, + config, + component_definition, + compilation_result, + platform, + callbacks: Vec::new(), + active_pointer_surface_id: None, + keyboard_focus_surface_id: None, + current_pointer_position: LogicalPosition::new(0.0, 0.0), + accumulated_axis_x: 0.0, + accumulated_axis_y: 0.0, + } + } + + #[must_use] + pub const fn state(&self) -> LockState { + self.state + } + + pub fn activate( + &mut self, + outputs: impl IntoIterator, + queue_handle: &QueueHandle, + ) -> Result<()> { + if !self.state.can_activate() { + return Err(LayerShikaError::InvalidInput { + message: format!("Session lock cannot activate in state {:?}", self.state), + }); + } + + self.config.validate()?; + + let session_lock = self.context.lock_manager().lock(queue_handle, ()); + self.session_lock = Some(session_lock.clone()); + self.state = LockState::Locking; + + for output in outputs { + let params = LockSurfaceParams { + compositor: self.context.compositor(), + output: &output, + session_lock: &session_lock, + fractional_scale_manager: self.context.fractional_scale_manager(), + viewporter: self.context.viewporter(), + queue_handle, + }; + let surface = LockSurface::create(¶ms); + let surface_id = surface.surface_id(); + let window = self.create_window(&surface_id)?; + self.lock_surfaces + .insert(output.id(), ActiveLockSurface::new(surface, window)); + } + + Ok(()) + } + + pub fn handle_locked(&mut self) { + if self.state == LockState::Locking { + info!("Session lock transitioned to Locked"); + self.state = LockState::Locked; + } + } + + pub fn deactivate(&mut self) -> Result<()> { + if !self.state.can_deactivate() { + return Err(LayerShikaError::InvalidInput { + message: format!("Session lock cannot deactivate in state {:?}", self.state), + }); + } + + let Some(session_lock) = self.session_lock.take() else { + return Err(LayerShikaError::InvalidInput { + message: "Session lock object missing during deactivate".to_string(), + }); + }; + + for surface in self.lock_surfaces.values() { + surface.surface.destroy(); + } + session_lock.unlock_and_destroy(); + self.lock_surfaces.clear(); + self.active_pointer_surface_id = None; + self.keyboard_focus_surface_id = None; + self.state = LockState::Unlocking; + Ok(()) + } + + pub fn handle_finished(&mut self) { + info!("Session lock finished"); + self.lock_surfaces.clear(); + self.session_lock = None; + self.state = LockState::Inactive; + self.active_pointer_surface_id = None; + self.keyboard_focus_surface_id = None; + } + + pub fn add_output( + &mut self, + output: &WlOutput, + queue_handle: &QueueHandle, + ) -> Result<()> { + if self.state != LockState::Locked { + return Ok(()); + } + + let output_id = output.id(); + if self.lock_surfaces.contains_key(&output_id) { + return Ok(()); + } + + let Some(session_lock) = self.session_lock.as_ref() else { + return Err(LayerShikaError::InvalidInput { + message: "Session lock object missing during output hotplug".to_string(), + }); + }; + + info!("Adding lock surface for output {output_id:?}"); + let params = LockSurfaceParams { + compositor: self.context.compositor(), + output, + session_lock, + fractional_scale_manager: self.context.fractional_scale_manager(), + viewporter: self.context.viewporter(), + queue_handle, + }; + let surface = LockSurface::create(¶ms); + let surface_id = surface.surface_id(); + let window = self.create_window(&surface_id)?; + self.lock_surfaces + .insert(output_id, ActiveLockSurface::new(surface, window)); + Ok(()) + } + + pub fn remove_output(&mut self, output_id: &ObjectId) { + if let Some(surface) = self.lock_surfaces.remove(output_id) { + let surface_id = surface.surface.surface_id(); + if self.active_pointer_surface_id.as_ref() == Some(&surface_id) { + self.active_pointer_surface_id = None; + } + if self.keyboard_focus_surface_id.as_ref() == Some(&surface_id) { + self.keyboard_focus_surface_id = None; + } + drop(surface); + } + } + + fn find_surface_by_lock_surface_id_mut( + &mut self, + lock_surface_id: &ObjectId, + ) -> Option<&mut ActiveLockSurface> { + self.lock_surfaces + .values_mut() + .find(|surface| surface.surface.lock_surface_id() == *lock_surface_id) + } + + fn find_surface_by_surface_id(&self, surface_id: &ObjectId) -> Option<&ActiveLockSurface> { + self.lock_surfaces + .values() + .find(|surface| surface.surface.surface_id() == *surface_id) + } + + pub fn handle_configure( + &mut self, + lock_surface_id: &ObjectId, + serial: u32, + width: u32, + height: u32, + ) -> Result<()> { + let context = LockConfigureContext { + scale_factor: self.config.scale_factor.value(), + component_definition: self.component_definition.clone(), + compilation_result: self.compilation_result.clone(), + platform: Rc::clone(&self.platform), + callbacks: self.callbacks.clone(), + }; + + let Some(surface) = self.find_surface_by_lock_surface_id_mut(lock_surface_id) else { + return Ok(()); + }; + + surface.handle_configure(serial, width, height, &context) + } + + pub fn render_frames(&self) -> Result<()> { + for surface in self.lock_surfaces.values() { + surface.render_frame_if_dirty()?; + } + Ok(()) + } + + pub(crate) fn register_callback(&mut self, callback: LockCallback) { + for surface in self.lock_surfaces.values() { + surface.apply_callback(&callback); + } + self.callbacks.push(callback); + } + + pub fn handle_fractional_scale(&mut self, fractional_scale_id: &ObjectId, scale_120ths: u32) { + for surface in self.lock_surfaces.values_mut() { + let matches = surface + .surface + .fractional_scale() + .is_some_and(|fs| fs.id() == *fractional_scale_id); + if matches { + surface.handle_fractional_scale(scale_120ths); + } + } + } + + pub fn is_lock_surface(&self, surface_id: &ObjectId) -> bool { + self.find_surface_by_surface_id(surface_id).is_some() + } + + pub const fn has_active_pointer(&self) -> bool { + self.active_pointer_surface_id.is_some() + } + + pub const fn has_keyboard_focus(&self) -> bool { + self.keyboard_focus_surface_id.is_some() + } + + pub fn handle_pointer_enter( + &mut self, + _serial: u32, + surface: &WlSurface, + surface_x: f64, + surface_y: f64, + ) -> bool { + let surface_id = surface.id(); + let (position, window) = { + let Some(active_surface) = self.find_surface_by_surface_id(&surface_id) else { + return false; + }; + ( + active_surface.to_logical_position(surface_x, surface_y), + active_surface.window_rc(), + ) + }; + + self.active_pointer_surface_id = Some(surface_id.clone()); + self.current_pointer_position = position; + info!("Lock pointer enter on {:?}", surface_id); + window + .window() + .dispatch_event(WindowEvent::PointerMoved { position }); + true + } + + pub fn handle_pointer_motion(&mut self, surface_x: f64, surface_y: f64) -> bool { + let Some(surface_id) = self.active_pointer_surface_id.clone() else { + return false; + }; + let (position, window) = { + let Some(active_surface) = self.find_surface_by_surface_id(&surface_id) else { + return false; + }; + ( + active_surface.to_logical_position(surface_x, surface_y), + active_surface.window_rc(), + ) + }; + + self.current_pointer_position = position; + window + .window() + .dispatch_event(WindowEvent::PointerMoved { position }); + true + } + + pub fn handle_pointer_leave(&mut self) -> bool { + let Some(surface_id) = self.active_pointer_surface_id.take() else { + return false; + }; + + if let Some(active_surface) = self.find_surface_by_surface_id(&surface_id) { + active_surface.dispatch_event(WindowEvent::PointerExited); + } + true + } + + pub fn handle_pointer_button( + &mut self, + _serial: u32, + button: u32, + button_state: WEnum, + ) -> bool { + let Some(surface_id) = self.active_pointer_surface_id.clone() else { + return false; + }; + let window = { + let Some(active_surface) = self.find_surface_by_surface_id(&surface_id) else { + return false; + }; + active_surface.window_rc() + }; + + let position = self.current_pointer_position; + let slint_button = wayland_button_to_slint(button); + let event = match button_state { + WEnum::Value(wl_pointer::ButtonState::Pressed) => WindowEvent::PointerPressed { + button: slint_button, + position, + }, + WEnum::Value(wl_pointer::ButtonState::Released) => WindowEvent::PointerReleased { + button: slint_button, + position, + }, + _ => return true, + }; + + info!( + "Lock pointer button {:?} at {:?} (scale {})", + button_state, + position, + self.config.scale_factor.value() + ); + window.window().dispatch_event(event); + true + } + + pub fn handle_axis_source(&mut self, _axis_source: wl_pointer::AxisSource) -> bool { + if self.active_pointer_surface_id.is_none() { + return false; + } + true + } + + pub fn handle_axis(&mut self, axis: wl_pointer::Axis, value: f64) -> bool { + if self.active_pointer_surface_id.is_none() { + return false; + } + + match axis { + wl_pointer::Axis::HorizontalScroll => { + #[allow(clippy::cast_possible_truncation)] + let delta = value as f32; + self.accumulated_axis_x += delta; + } + wl_pointer::Axis::VerticalScroll => { + #[allow(clippy::cast_possible_truncation)] + let delta = value as f32; + self.accumulated_axis_y += delta; + } + _ => {} + } + true + } + + pub fn handle_axis_discrete(&mut self, axis: wl_pointer::Axis, discrete: i32) -> bool { + if self.active_pointer_surface_id.is_none() { + return false; + } + + #[allow(clippy::cast_precision_loss)] + let delta = (discrete as f32) * 60.0; + match axis { + wl_pointer::Axis::HorizontalScroll => { + self.accumulated_axis_x += delta; + } + wl_pointer::Axis::VerticalScroll => { + self.accumulated_axis_y += delta; + } + _ => {} + } + true + } + + pub fn handle_axis_stop(&mut self, _axis: wl_pointer::Axis) -> bool { + self.active_pointer_surface_id.is_some() + } + + pub fn handle_pointer_frame(&mut self) -> bool { + let Some(surface_id) = self.active_pointer_surface_id.clone() else { + return false; + }; + let delta_x = self.accumulated_axis_x; + let delta_y = self.accumulated_axis_y; + self.accumulated_axis_x = 0.0; + self.accumulated_axis_y = 0.0; + + let window = { + let Some(active_surface) = self.find_surface_by_surface_id(&surface_id) else { + return false; + }; + active_surface.window_rc() + }; + + if delta_x.abs() > f32::EPSILON || delta_y.abs() > f32::EPSILON { + let position = self.current_pointer_position; + window + .window() + .dispatch_event(WindowEvent::PointerScrolled { + position, + delta_x, + delta_y, + }); + } + + true + } + + pub fn handle_keyboard_enter(&mut self, surface: &WlSurface) -> bool { + let surface_id = surface.id(); + if self.find_surface_by_surface_id(&surface_id).is_some() { + self.keyboard_focus_surface_id = Some(surface_id); + return true; + } + false + } + + pub fn handle_keyboard_leave(&mut self, surface: &WlSurface) -> bool { + let surface_id = surface.id(); + if self.keyboard_focus_surface_id.as_ref() == Some(&surface_id) { + self.keyboard_focus_surface_id = None; + return true; + } + false + } + + pub fn handle_keyboard_key( + &mut self, + key: u32, + state: wl_keyboard::KeyState, + keyboard_state: &mut KeyboardState, + ) -> bool { + let Some(surface_id) = self.keyboard_focus_surface_id.clone() else { + return false; + }; + let Some(active_surface) = self.find_surface_by_surface_id(&surface_id) else { + return false; + }; + let Some(xkb_state) = keyboard_state.xkb_state.as_mut() else { + return true; + }; + + let keycode = xkb::Keycode::new(key + 8); + let direction = match state { + wl_keyboard::KeyState::Pressed => xkb::KeyDirection::Down, + wl_keyboard::KeyState::Released => xkb::KeyDirection::Up, + _ => return true, + }; + + xkb_state.update_key(keycode, direction); + + let text = xkb_state.key_get_utf8(keycode); + let text = if text.is_empty() { + let keysym = xkb_state.key_get_one_sym(keycode); + keysym_to_text(keysym) + } else { + Some(SharedString::from(text.as_str())) + }; + + let Some(text) = text else { + return true; + }; + + let event = match state { + wl_keyboard::KeyState::Pressed => WindowEvent::KeyPressed { text }, + wl_keyboard::KeyState::Released => WindowEvent::KeyReleased { text }, + _ => return true, + }; + info!("Lock key event {:?}", state); + active_surface.dispatch_event(event); + true + } + + fn create_window(&self, surface_id: &ObjectId) -> Result> { + let init_size = LogicalSize::new(1.0, 1.0); + let context = self.context.render_factory().create_context( + surface_id, + init_size.to_physical(self.config.scale_factor.value()), + )?; + let renderer = FemtoVGRenderer::new(context) + .map_err(|e| LayerShikaError::FemtoVGRendererCreation { source: e })?; + let window = FemtoVGWindow::new(renderer); + RenderableWindow::set_scale_factor(window.as_ref(), self.config.scale_factor.value()); + window.set_size(WindowSize::Logical(init_size)); + window.set_position(WindowPosition::Logical(LogicalPosition::new(0., 0.))); + Ok(window) + } +} diff --git a/crates/adapters/src/wayland/session_lock/lock_surface.rs b/crates/adapters/src/wayland/session_lock/lock_surface.rs new file mode 100644 index 0000000..4dd220f --- /dev/null +++ b/crates/adapters/src/wayland/session_lock/lock_surface.rs @@ -0,0 +1,118 @@ +use crate::wayland::session_lock::lock_context::LockSurfaceParams; +use log::info; +use std::rc::Rc; +use wayland_client::{Proxy, backend::ObjectId, protocol::wl_surface::WlSurface}; +use wayland_protocols::ext::session_lock::v1::client::ext_session_lock_surface_v1::ExtSessionLockSurfaceV1; +use wayland_protocols::wp::fractional_scale::v1::client::{ + wp_fractional_scale_v1::WpFractionalScaleV1, +}; +use wayland_protocols::wp::viewporter::client::wp_viewport::WpViewport; + +pub struct LockSurface { + surface: Rc, + session_surface: Rc, + fractional_scale: Option>, + viewport: Option>, + width: u32, + height: u32, + configured: bool, +} + +impl LockSurface { + pub fn create(params: &LockSurfaceParams<'_>) -> Self { + let surface = Rc::new(params.compositor.create_surface(params.queue_handle, ())); + + let session_surface = Rc::new(params.session_lock.get_lock_surface( + &surface, + params.output, + params.queue_handle, + (), + )); + + let fractional_scale = params.fractional_scale_manager.map(|manager| { + info!("Creating fractional scale object for lock surface"); + Rc::new(manager.get_fractional_scale(&surface, params.queue_handle, ())) + }); + + let viewport = params.viewporter.map(|vp| { + info!("Creating viewport for lock surface"); + Rc::new(vp.get_viewport(&surface, params.queue_handle, ())) + }); + + surface.set_buffer_scale(1); + + Self { + surface, + session_surface, + fractional_scale, + viewport, + width: 0, + height: 0, + configured: false, + } + } + + pub fn handle_configure(&mut self, serial: u32, width: u32, height: u32) { + info!("Lock surface configured with compositor size: {width}x{height}"); + self.session_surface.ack_configure(serial); + self.width = width; + self.height = height; + self.configured = true; + } + + #[must_use] + pub const fn width(&self) -> u32 { + self.width + } + + #[must_use] + pub const fn height(&self) -> u32 { + self.height + } + + #[must_use] + pub fn surface_id(&self) -> ObjectId { + self.surface.id() + } + + #[must_use] + pub fn lock_surface_id(&self) -> ObjectId { + self.session_surface.id() + } + + pub fn fractional_scale(&self) -> Option<&Rc> { + self.fractional_scale.as_ref() + } + + pub const fn has_fractional_scale(&self) -> bool { + self.fractional_scale.is_some() + } + + pub const fn has_viewport(&self) -> bool { + self.viewport.is_some() + } + + pub fn configure_fractional_viewport(&self, logical_width: u32, logical_height: u32) { + self.surface.set_buffer_scale(1); + if let Some(vp) = &self.viewport { + let width_i32 = i32::try_from(logical_width).unwrap_or(i32::MAX); + let height_i32 = i32::try_from(logical_height).unwrap_or(i32::MAX); + vp.set_destination(width_i32, height_i32); + } + } + + pub fn configure_buffer_scale(&self, buffer_scale: i32) { + self.surface.set_buffer_scale(buffer_scale); + } + + pub fn destroy(&self) { + self.session_surface.destroy(); + self.surface.destroy(); + } +} + +impl Drop for LockSurface { + fn drop(&mut self) { + self.destroy(); + } +} diff --git a/crates/adapters/src/wayland/session_lock/mod.rs b/crates/adapters/src/wayland/session_lock/mod.rs new file mode 100644 index 0000000..0a68455 --- /dev/null +++ b/crates/adapters/src/wayland/session_lock/mod.rs @@ -0,0 +1,3 @@ +pub mod lock_context; +pub mod lock_manager; +pub mod lock_surface; diff --git a/crates/adapters/src/wayland/shell_adapter.rs b/crates/adapters/src/wayland/shell_adapter.rs index b269c42..eab5d80 100644 --- a/crates/adapters/src/wayland/shell_adapter.rs +++ b/crates/adapters/src/wayland/shell_adapter.rs @@ -26,6 +26,8 @@ use crate::{ use core::result::Result as CoreResult; use layer_shika_domain::errors::DomainError; use layer_shika_domain::ports::shell::ShellSystemPort; +use layer_shika_domain::value_objects::lock_config::LockConfig; +use layer_shika_domain::value_objects::lock_state::LockState; use layer_shika_domain::value_objects::output_handle::OutputHandle; use layer_shika_domain::value_objects::output_info::OutputInfo; use log::{error, info}; @@ -272,7 +274,10 @@ impl WaylandShellSystem { connection: &Connection, event_queue: &mut EventQueue, ) -> Result { - let global_ctx = GlobalContext::initialize(connection, &event_queue.handle())?; + let global_ctx = Rc::new(GlobalContext::initialize( + connection, + &event_queue.handle(), + )?); let layer_surface_config = Self::create_layer_surface_config(config); let pointer = Rc::new(global_ctx.seat.get_pointer(&event_queue.handle(), ())); @@ -300,7 +305,7 @@ impl WaylandShellSystem { let setups = Self::create_output_setups( config, - &global_ctx, + global_ctx.as_ref(), connection, event_queue, &pointer, @@ -308,6 +313,7 @@ impl WaylandShellSystem { )?; let platform = Self::setup_platform(&setups)?; + app_state.set_slint_platform(Rc::clone(&platform)); let (popup_managers, layer_surfaces) = Self::create_window_states(setups, &popup_context, &shared_serial, &mut app_state)?; @@ -322,7 +328,7 @@ impl WaylandShellSystem { let output_manager = Self::create_output_manager(&OutputManagerParams { config, - global_ctx: &global_ctx, + global_ctx: global_ctx.as_ref(), connection, layer_surface_config, render_factory: &render_factory, @@ -332,6 +338,7 @@ impl WaylandShellSystem { }); app_state.set_output_manager(Rc::new(RefCell::new(output_manager))); + app_state.set_global_context(Rc::clone(&global_ctx)); Ok(app_state) } @@ -341,7 +348,10 @@ impl WaylandShellSystem { connection: &Connection, event_queue: &mut EventQueue, ) -> Result { - let global_ctx = GlobalContext::initialize(connection, &event_queue.handle())?; + let global_ctx = Rc::new(GlobalContext::initialize( + connection, + &event_queue.handle(), + )?); let pointer = Rc::new(global_ctx.seat.get_pointer(&event_queue.handle(), ())); let keyboard = Rc::new(global_ctx.seat.get_keyboard(&event_queue.handle(), ())); @@ -368,13 +378,14 @@ impl WaylandShellSystem { let setups = Self::create_output_setups_multi( configs, - &global_ctx, + global_ctx.as_ref(), connection, event_queue, &pointer, )?; let platform = Self::setup_platform(&setups)?; + app_state.set_slint_platform(Rc::clone(&platform)); let (popup_managers, layer_surfaces) = Self::create_window_states(setups, &popup_context, &shared_serial, &mut app_state)?; @@ -392,7 +403,7 @@ impl WaylandShellSystem { let layer_surface_config = Self::create_layer_surface_config(config); let output_manager = Self::create_output_manager(&OutputManagerParams { config, - global_ctx: &global_ctx, + global_ctx: global_ctx.as_ref(), connection, layer_surface_config, render_factory: &render_factory, @@ -404,6 +415,8 @@ impl WaylandShellSystem { app_state.set_output_manager(Rc::new(RefCell::new(output_manager))); } + app_state.set_global_context(Rc::clone(&global_ctx)); + Ok(app_state) } @@ -611,6 +624,8 @@ impl WaylandShellSystem { } })?; } + + self.state.render_lock_frames()?; } info!("Initial configuration complete, requesting final render"); @@ -626,6 +641,7 @@ impl WaylandShellSystem { message: e.to_string(), })?; } + self.state.render_lock_frames()?; self.connection .flush() .map_err(|e| LayerShikaError::WaylandProtocol { source: e })?; @@ -694,6 +710,8 @@ impl WaylandShellSystem { } } + shared_data.render_lock_frames()?; + connection .flush() .map_err(|e| LayerShikaError::WaylandProtocol { source: e })?; @@ -780,6 +798,33 @@ impl WaylandSystemOps for WaylandShellSystem { WaylandShellSystem::despawn_surface(self, name) } + fn activate_session_lock(&mut self, component_name: &str, config: LockConfig) -> Result<()> { + let queue_handle = self.event_queue.handle(); + self.state + .activate_session_lock(component_name, config, &queue_handle) + } + + fn deactivate_session_lock(&mut self) -> Result<()> { + self.state.deactivate_session_lock() + } + + fn is_session_lock_available(&self) -> bool { + self.state.is_session_lock_available() + } + + fn session_lock_state(&self) -> Option { + self.state.current_lock_state() + } + + fn register_session_lock_callback( + &mut self, + callback_name: &str, + handler: Rc slint_interpreter::Value>, + ) { + self.state + .register_session_lock_callback(callback_name, handler); + } + fn app_state(&self) -> &AppState { WaylandShellSystem::app_state(self) } diff --git a/crates/adapters/src/wayland/surfaces/app_state.rs b/crates/adapters/src/wayland/surfaces/app_state.rs index 1421ff7..56e7e15 100644 --- a/crates/adapters/src/wayland/surfaces/app_state.rs +++ b/crates/adapters/src/wayland/surfaces/app_state.rs @@ -1,12 +1,21 @@ use super::event_context::SharedPointerSerial; use super::keyboard_state::KeyboardState; use super::surface_state::SurfaceState; +use crate::errors::{LayerShikaError, Result}; +use crate::rendering::egl::context_factory::RenderContextFactory; +use crate::wayland::globals::context::GlobalContext; use crate::wayland::managed_proxies::{ManagedWlKeyboard, ManagedWlPointer}; use crate::wayland::outputs::{OutputManager, OutputMapping}; +use crate::wayland::session_lock::lock_context::SessionLockContext; +use crate::wayland::session_lock::lock_manager::{LockCallback, SessionLockManager}; +use crate::rendering::slint_integration::platform::CustomSlintPlatform; use layer_shika_domain::entities::output_registry::OutputRegistry; use layer_shika_domain::value_objects::handle::SurfaceHandle; use layer_shika_domain::value_objects::output_handle::OutputHandle; +use layer_shika_domain::value_objects::lock_config::LockConfig; +use layer_shika_domain::value_objects::lock_state::LockState; use layer_shika_domain::value_objects::output_info::OutputInfo; +use slint_interpreter::{CompilationResult, ComponentDefinition, Value}; use std::cell::RefCell; use std::collections::HashMap; use std::os::fd::BorrowedFd; @@ -14,10 +23,11 @@ use std::rc::Rc; use wayland_client::Proxy; use wayland_client::backend::ObjectId; use wayland_client::protocol::wl_keyboard; -use wayland_client::protocol::wl_surface::WlSurface; +use wayland_client::protocol::{wl_output::WlOutput, wl_surface::WlSurface}; use xkbcommon::xkb; pub type PerOutputSurface = SurfaceState; +type SessionLockCallback = Rc Value>; #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct ShellSurfaceKey { @@ -35,6 +45,9 @@ impl ShellSurfaceKey { } pub struct AppState { + global_context: Option>, + known_outputs: Vec, + slint_platform: Option>, output_registry: OutputRegistry, output_mapping: OutputMapping, surfaces: HashMap, @@ -49,6 +62,8 @@ pub struct AppState { keyboard_focus_key: Option, keyboard_focus_surface_id: Option, keyboard_state: KeyboardState, + lock_manager: Option, + lock_callbacks: Vec, } impl AppState { @@ -58,6 +73,9 @@ impl AppState { shared_serial: Rc, ) -> Self { Self { + global_context: None, + known_outputs: Vec::new(), + slint_platform: None, output_registry: OutputRegistry::new(), output_mapping: OutputMapping::new(), surfaces: HashMap::new(), @@ -72,6 +90,194 @@ impl AppState { keyboard_focus_key: None, keyboard_focus_surface_id: None, keyboard_state: KeyboardState::new(), + lock_manager: None, + lock_callbacks: Vec::new(), + } + } + + pub fn set_global_context(&mut self, context: Rc) { + self.known_outputs.clone_from(&context.outputs); + self.global_context = Some(context); + } + + pub fn set_slint_platform(&mut self, platform: Rc) { + self.slint_platform = Some(platform); + } + + pub fn lock_manager(&self) -> Option<&SessionLockManager> { + self.lock_manager.as_ref() + } + + pub fn lock_manager_mut(&mut self) -> Option<&mut SessionLockManager> { + self.lock_manager.as_mut() + } + + pub fn clear_lock_manager(&mut self) { + self.lock_manager = None; + } + + pub fn is_session_lock_available(&self) -> bool { + self.global_context + .as_ref() + .and_then(|ctx| ctx.session_lock_manager.as_ref()) + .is_some() + } + + pub fn current_lock_state(&self) -> Option { + self.lock_manager.as_ref().map(SessionLockManager::state) + } + + pub fn register_session_lock_callback( + &mut self, + callback_name: impl Into, + handler: SessionLockCallback, + ) { + let callback = LockCallback::new(callback_name, handler); + if let Some(manager) = self.lock_manager.as_mut() { + manager.register_callback(callback.clone()); + } + self.lock_callbacks.push(callback); + } + + pub fn activate_session_lock( + &mut self, + component_name: &str, + config: LockConfig, + queue_handle: &wayland_client::QueueHandle, + ) -> Result<()> { + if self.lock_manager.is_some() { + return Err(LayerShikaError::InvalidInput { + message: "Session lock already active".to_string(), + }); + } + + let context = self.create_lock_context()?; + let (definition, compilation_result) = + self.resolve_lock_component(component_name)?; + let platform = self.slint_platform.as_ref().ok_or_else(|| { + LayerShikaError::InvalidInput { + message: "Slint platform not initialized".to_string(), + } + })?; + let mut manager = SessionLockManager::new( + context, + definition, + compilation_result, + Rc::clone(platform), + config, + ); + for callback in self.lock_callbacks.iter().cloned() { + manager.register_callback(callback); + } + + let outputs = self.collect_session_lock_outputs(); + manager.activate(outputs, queue_handle)?; + + self.lock_manager = Some(manager); + Ok(()) + } + + pub fn deactivate_session_lock(&mut self) -> Result<()> { + let Some(manager) = self.lock_manager.as_mut() else { + return Err(LayerShikaError::InvalidInput { + message: "No session lock active".to_string(), + }); + }; + + manager.deactivate() + } + + pub fn render_lock_frames(&self) -> Result<()> { + if let Some(manager) = self.lock_manager.as_ref() { + if manager.state() != LockState::Locked && manager.state() != LockState::Locking { + return Ok(()); + } + manager.render_frames()?; + } + Ok(()) + } + + fn resolve_lock_component( + &self, + component_name: &str, + ) -> Result<(ComponentDefinition, Option>)> { + let compilation_result = self + .primary_output() + .and_then(SurfaceState::compilation_result) + .ok_or_else(|| LayerShikaError::InvalidInput { + message: "No compilation result available for session lock".to_string(), + })?; + + let definition = compilation_result.component(component_name).ok_or_else(|| { + LayerShikaError::InvalidInput { + message: format!( + "Component '{component_name}' not found in compilation result" + ), + } + })?; + + Ok((definition, Some(compilation_result))) + } + + fn create_lock_context(&self) -> Result> { + let Some(global_ctx) = self.global_context.as_ref() else { + return Err(LayerShikaError::InvalidInput { + message: "Global context not available for session lock".to_string(), + }); + }; + + let Some(lock_manager) = global_ctx.session_lock_manager.as_ref() else { + return Err(LayerShikaError::InvalidInput { + message: "Session lock protocol not available".to_string(), + }); + }; + + let render_factory = + RenderContextFactory::new(Rc::clone(&global_ctx.render_context_manager)); + + Ok(Rc::new(SessionLockContext::new( + global_ctx.compositor.clone(), + lock_manager.clone(), + global_ctx.seat.clone(), + global_ctx.fractional_scale_manager.clone(), + global_ctx.viewporter.clone(), + render_factory, + ))) + } + + fn collect_session_lock_outputs(&self) -> Vec { + self.known_outputs.clone() + } + + pub fn handle_output_added_for_lock( + &mut self, + output: &WlOutput, + queue_handle: &wayland_client::QueueHandle, + ) -> Result<()> { + if !self + .known_outputs + .iter() + .any(|known| known.id() == output.id()) + { + self.known_outputs.push(output.clone()); + } + + let Some(manager) = self.lock_manager.as_mut() else { + return Ok(()); + }; + + if manager.state() == LockState::Locked { + manager.add_output(output, queue_handle)?; + } + + Ok(()) + } + + pub fn handle_output_removed_for_lock(&mut self, output_id: &ObjectId) { + self.known_outputs + .retain(|output| output.id() != *output_id); + if let Some(manager) = self.lock_manager.as_mut() { + manager.remove_output(output_id); } } @@ -360,6 +566,13 @@ impl AppState { } pub fn handle_keyboard_enter(&mut self, _serial: u32, surface: &WlSurface, _keys: &[u8]) { + if let Some(manager) = self.lock_manager.as_mut() { + if manager.handle_keyboard_enter(surface) { + self.set_keyboard_focus(None, None); + return; + } + } + let surface_id = surface.id(); if let Some(key) = self.get_key_by_surface(&surface_id).cloned() { self.set_keyboard_focus(Some(key), Some(surface_id)); @@ -372,12 +585,24 @@ impl AppState { } pub fn handle_keyboard_leave(&mut self, _serial: u32, surface: &WlSurface) { + if let Some(manager) = self.lock_manager.as_mut() { + if manager.handle_keyboard_leave(surface) { + return; + } + } + if self.keyboard_focus_surface_id == Some(surface.id()) { self.set_keyboard_focus(None, None); } } pub fn handle_key(&mut self, _serial: u32, _time: u32, key: u32, state: wl_keyboard::KeyState) { + if let Some(manager) = self.lock_manager.as_mut() { + if manager.handle_keyboard_key(key, state, &mut self.keyboard_state) { + return; + } + } + let Some(focus_key) = self.keyboard_focus_key.clone() else { return; }; @@ -385,9 +610,8 @@ impl AppState { return; }; - let keyboard_state = &mut self.keyboard_state; if let Some(surface) = self.surfaces.get_mut(&focus_key) { - surface.handle_keyboard_key(&surface_id, key, state, keyboard_state); + surface.handle_keyboard_key(&surface_id, key, state, &mut self.keyboard_state); } } diff --git a/crates/adapters/src/wayland/surfaces/keyboard_state.rs b/crates/adapters/src/wayland/surfaces/keyboard_state.rs index d130e44..b3d5713 100644 --- a/crates/adapters/src/wayland/surfaces/keyboard_state.rs +++ b/crates/adapters/src/wayland/surfaces/keyboard_state.rs @@ -1,3 +1,4 @@ +use slint::{SharedString, platform::Key}; use xkbcommon::xkb; pub struct KeyboardState { @@ -25,3 +26,48 @@ impl KeyboardState { self.xkb_keymap = Some(keymap); } } + +pub(crate) fn keysym_to_text(keysym: xkb::Keysym) -> Option { + let key = match keysym.raw() { + xkb::keysyms::KEY_Return | xkb::keysyms::KEY_KP_Enter => Key::Return, + xkb::keysyms::KEY_BackSpace => Key::Backspace, + xkb::keysyms::KEY_Tab => Key::Tab, + xkb::keysyms::KEY_BackTab => Key::Backtab, + xkb::keysyms::KEY_Escape => Key::Escape, + xkb::keysyms::KEY_Delete => Key::Delete, + xkb::keysyms::KEY_Insert => Key::Insert, + xkb::keysyms::KEY_Home => Key::Home, + xkb::keysyms::KEY_End => Key::End, + xkb::keysyms::KEY_Page_Up => Key::PageUp, + xkb::keysyms::KEY_Page_Down => Key::PageDown, + xkb::keysyms::KEY_Left => Key::LeftArrow, + xkb::keysyms::KEY_Right => Key::RightArrow, + xkb::keysyms::KEY_Up => Key::UpArrow, + xkb::keysyms::KEY_Down => Key::DownArrow, + xkb::keysyms::KEY_space => Key::Space, + xkb::keysyms::KEY_Shift_L => Key::Shift, + xkb::keysyms::KEY_Shift_R => Key::ShiftR, + xkb::keysyms::KEY_Control_L => Key::Control, + xkb::keysyms::KEY_Control_R => Key::ControlR, + xkb::keysyms::KEY_Alt_L | xkb::keysyms::KEY_Alt_R => Key::Alt, + xkb::keysyms::KEY_Mode_switch => Key::AltGr, + xkb::keysyms::KEY_Meta_L => Key::Meta, + xkb::keysyms::KEY_Meta_R => Key::MetaR, + xkb::keysyms::KEY_Caps_Lock => Key::CapsLock, + xkb::keysyms::KEY_F1 => Key::F1, + xkb::keysyms::KEY_F2 => Key::F2, + xkb::keysyms::KEY_F3 => Key::F3, + xkb::keysyms::KEY_F4 => Key::F4, + xkb::keysyms::KEY_F5 => Key::F5, + xkb::keysyms::KEY_F6 => Key::F6, + xkb::keysyms::KEY_F7 => Key::F7, + xkb::keysyms::KEY_F8 => Key::F8, + xkb::keysyms::KEY_F9 => Key::F9, + xkb::keysyms::KEY_F10 => Key::F10, + xkb::keysyms::KEY_F11 => Key::F11, + xkb::keysyms::KEY_F12 => Key::F12, + _ => return None, + }; + + Some(SharedString::from(key)) +} diff --git a/crates/adapters/src/wayland/surfaces/mod.rs b/crates/adapters/src/wayland/surfaces/mod.rs index ec7a3cc..4a015df 100644 --- a/crates/adapters/src/wayland/surfaces/mod.rs +++ b/crates/adapters/src/wayland/surfaces/mod.rs @@ -5,6 +5,7 @@ pub mod display_metrics; pub mod event_context; pub mod keyboard_state; pub mod layer_surface; +pub(crate) mod pointer_utils; pub mod popup_manager; pub mod popup_surface; pub mod rendering_state; diff --git a/crates/adapters/src/wayland/surfaces/pointer_utils.rs b/crates/adapters/src/wayland/surfaces/pointer_utils.rs new file mode 100644 index 0000000..0d35c63 --- /dev/null +++ b/crates/adapters/src/wayland/surfaces/pointer_utils.rs @@ -0,0 +1,12 @@ +use slint::platform::PointerEventButton; + +pub(crate) fn wayland_button_to_slint(button: u32) -> PointerEventButton { + match button { + 0x110 => PointerEventButton::Left, + 0x111 => PointerEventButton::Right, + 0x112 => PointerEventButton::Middle, + 0x115 => PointerEventButton::Forward, + 0x116 => PointerEventButton::Back, + _ => PointerEventButton::Other, + } +} diff --git a/crates/composition/src/lib.rs b/crates/composition/src/lib.rs index 6c7d8c1..a47c6b7 100644 --- a/crates/composition/src/lib.rs +++ b/crates/composition/src/lib.rs @@ -4,6 +4,7 @@ mod event_loop; mod layer_surface; mod popup; mod popup_builder; +mod session_lock; mod selection; mod selector; mod shell; @@ -40,6 +41,7 @@ pub use layer_shika_domain::value_objects::{ pub use layer_surface::{LayerSurfaceHandle, ShellSurfaceConfigHandler}; pub use popup::PopupShell; pub use popup_builder::PopupBuilder; +pub use session_lock::{SessionLock, SessionLockBuilder}; pub use selection::Selection; pub use selector::{Output, Selector, Surface, SurfaceInfo}; pub use shell_runtime::{DEFAULT_SURFACE_NAME, ShellRuntime}; @@ -75,6 +77,12 @@ pub enum Error { #[error("App has been dropped")] SystemDropped, + + #[error("Invalid lock state '{current}' for operation '{operation}'")] + InvalidState { current: String, operation: String }, + + #[error("Protocol '{protocol}' not available on this compositor")] + ProtocolNotAvailable { protocol: String }, } pub mod prelude { @@ -83,10 +91,11 @@ pub mod prelude { DEFAULT_SURFACE_NAME, EventDispatchContext, EventLoopHandle, Handle, IntoValue, KeyboardInteractivity, Layer, LayerSurfaceHandle, Output, OutputGeometry, OutputHandle, OutputInfo, OutputPolicy, OutputRegistry, PopupBuilder, PopupConfig, PopupHandle, - PopupPosition, PopupShell, PopupSize, PopupWindow, Result, Selection, Selector, Shell, - ShellBuilder, ShellConfig, ShellControl, ShellEventContext, ShellEventLoop, ShellRuntime, - ShellSurfaceConfigHandler, Surface, SurfaceComponentConfig, SurfaceConfigBuilder, - SurfaceControlHandle, SurfaceDefinition, SurfaceEntry, SurfaceHandle, SurfaceInfo, + PopupPosition, PopupShell, PopupSize, PopupWindow, Result, Selection, Selector, + SessionLock, SessionLockBuilder, Shell, ShellBuilder, ShellConfig, ShellControl, + ShellEventContext, ShellEventLoop, ShellRuntime, ShellSurfaceConfigHandler, Surface, + SurfaceComponentConfig, SurfaceConfigBuilder, SurfaceControlHandle, SurfaceDefinition, + SurfaceEntry, SurfaceHandle, SurfaceInfo, SurfaceMetadata, SurfaceRegistry, }; diff --git a/crates/composition/src/session_lock.rs b/crates/composition/src/session_lock.rs new file mode 100644 index 0000000..9c982af --- /dev/null +++ b/crates/composition/src/session_lock.rs @@ -0,0 +1,190 @@ +use crate::{Error, Result}; +use crate::IntoValue; +use crate::calloop::channel; +use crate::system::{SessionLockCommand, ShellCommand}; +use layer_shika_adapters::WaylandSystemOps; +use layer_shika_domain::dimensions::ScaleFactor; +use layer_shika_domain::errors::DomainError; +use layer_shika_domain::value_objects::lock_config::LockConfig; +use layer_shika_domain::value_objects::lock_state::LockState; +use layer_shika_domain::value_objects::margins::Margins; +use layer_shika_domain::value_objects::output_policy::OutputPolicy; +use std::cell::{Cell, RefCell}; +use std::rc::Weak; +use std::rc::Rc; +use crate::slint_interpreter::Value; + +pub struct SessionLock { + system: Weak>, + component_name: String, + config: LockConfig, + state: Cell, + command_sender: channel::Sender, +} + +impl SessionLock { + pub(crate) fn new( + system: Weak>, + component_name: String, + config: LockConfig, + command_sender: channel::Sender, + ) -> Self { + Self { + system, + component_name, + config, + state: Cell::new(LockState::Inactive), + command_sender, + } + } + + pub fn activate(&self) -> Result<()> { + let current = self.state.get(); + if !current.can_activate() { + return Err(Error::InvalidState { + current: format!("{current:?}"), + operation: "activate".to_string(), + }); + } + + let system = self.system.upgrade().ok_or(Error::SystemDropped)?; + system + .borrow_mut() + .activate_session_lock(&self.component_name, self.config.clone())?; + self.state.set(LockState::Locking); + Ok(()) + } + + pub fn deactivate(&self) -> Result<()> { + log::info!("deactivate() called - queuing SessionLockCommand::Deactivate"); + + if let Some(system) = self.system.upgrade() { + if let Ok(borrowed) = system.try_borrow() { + if let Some(state) = borrowed.session_lock_state() { + log::info!("Syncing lock state before deactivate: {:?}", state); + self.state.set(state); + } + } else { + log::warn!("Could not borrow system to sync state - already borrowed"); + } + } + + let current = self.state.get(); + log::info!("Current lock state for deactivate check: {:?}", current); + if !current.can_deactivate() { + return Err(Error::InvalidState { + current: format!("{current:?}"), + operation: "deactivate".to_string(), + }); + } + + // Send deactivate command via channel to be processed outside borrow context + self.command_sender + .send(ShellCommand::SessionLock(SessionLockCommand::Deactivate)) + .map_err(|e| Error::Domain(DomainError::InvalidInput { + message: format!("Failed to send session lock command: {e:?}"), + }))?; + + log::info!("SessionLockCommand::Deactivate queued successfully"); + Ok(()) + } + + /// Registers a callback handler on the lock screen component. + /// + /// The callback must exist in the Slint component, for example: + /// `callback unlock_requested(string)`. + pub fn on_callback(&self, callback_name: &str, handler: F) -> Result<()> + where + F: Fn() -> R + Clone + 'static, + R: IntoValue, + { + self.on_callback_with_args(callback_name, move |_args| handler().into_value()) + } + + /// Registers a callback handler that receives Slint arguments. + pub fn on_callback_with_args(&self, callback_name: &str, handler: F) -> Result<()> + where + F: Fn(&[Value]) -> R + Clone + 'static, + R: IntoValue, + { + let system = self.system.upgrade().ok_or(Error::SystemDropped)?; + let handler = Rc::new(move |args: &[Value]| handler(args).into_value()); + system + .borrow_mut() + .register_session_lock_callback(callback_name, handler); + Ok(()) + } + + #[must_use] + pub fn state(&self) -> LockState { + if let Some(system) = self.system.upgrade() { + if let Some(state) = system.borrow().session_lock_state() { + self.state.set(state); + } + } + self.state.get() + } + + #[must_use] + pub fn is_locked(&self) -> bool { + self.state() == LockState::Locked + } + + #[must_use] + pub fn component_name(&self) -> &str { + &self.component_name + } + +} + +pub struct SessionLockBuilder { + component_name: String, + config: LockConfig, +} + +impl SessionLockBuilder { + #[must_use] + pub fn new(component_name: impl Into) -> Self { + Self { + component_name: component_name.into(), + config: LockConfig::default(), + } + } + + #[must_use] + pub fn scale_factor(mut self, factor: impl TryInto) -> Self { + self.config.scale_factor = factor.try_into().unwrap_or_default(); + self + } + + #[must_use] + pub fn margin(mut self, margin: impl Into) -> Self { + self.config.margin = margin.into(); + self + } + + #[must_use] + pub fn namespace(mut self, namespace: impl Into) -> Self { + self.config.namespace = namespace.into(); + self + } + + #[must_use] + pub fn output_policy(mut self, policy: OutputPolicy) -> Self { + self.config.output_policy = policy; + self + } + + pub(crate) fn build( + self, + system: Weak>, + command_sender: channel::Sender, + ) -> SessionLock { + SessionLock::new(system, self.component_name, self.config, command_sender) + } + + #[must_use] + pub fn component_name(&self) -> &str { + &self.component_name + } +} diff --git a/crates/composition/src/shell.rs b/crates/composition/src/shell.rs index fe859db..2f589c5 100644 --- a/crates/composition/src/shell.rs +++ b/crates/composition/src/shell.rs @@ -1,11 +1,12 @@ use crate::event_loop::{EventLoopHandle, FromAppState}; use crate::layer_surface::LayerSurfaceHandle; +use crate::session_lock::{SessionLock, SessionLockBuilder}; use crate::shell_config::{CompiledUiSource, ShellConfig}; use crate::shell_runtime::ShellRuntime; use crate::surface_registry::{SurfaceDefinition, SurfaceEntry, SurfaceRegistry}; use crate::system::{ - CallbackContext, EventDispatchContext, PopupCommand, ShellCommand, ShellControl, - SurfaceCommand, SurfaceTarget, + CallbackContext, EventDispatchContext, PopupCommand, SessionLockCommand, ShellCommand, + ShellControl, SurfaceCommand, SurfaceTarget, }; use crate::value_conversion::IntoValue; use crate::{Error, Result}; @@ -30,7 +31,7 @@ use layer_shika_domain::value_objects::surface_instance_id::SurfaceInstanceId; use spin_on::spin_on; use std::cell::RefCell; use std::path::{Path, PathBuf}; -use std::rc::Rc; +use std::rc::{Rc, Weak}; /// Default Slint component name used when none is specified pub const DEFAULT_COMPONENT_NAME: &str = "Main"; @@ -543,6 +544,7 @@ impl Shell { fn setup_command_handler(&self, receiver: channel::Channel) -> Result<()> { let loop_handle = self.inner.borrow().event_loop_handle(); let control = self.control(); + let system = Rc::downgrade(&self.inner); loop_handle .insert_source(receiver, move |event, (), app_state| { @@ -556,6 +558,9 @@ impl Shell { ShellCommand::Surface(surface_cmd) => { Self::handle_surface_command(surface_cmd, &mut ctx); } + ShellCommand::SessionLock(lock_cmd) => { + Self::handle_session_lock_command(&lock_cmd, &mut ctx, &system); + } ShellCommand::Render => { if let Err(e) = ctx.render_frame_if_dirty() { log::error!("Failed to render frame: {}", e); @@ -604,6 +609,23 @@ impl Shell { } } + fn handle_session_lock_command( + command: &SessionLockCommand, + ctx: &mut EventDispatchContext<'_>, + _system: &Weak>, + ) { + match command { + SessionLockCommand::Deactivate => { + log::info!("Processing SessionLockCommand::Deactivate"); + if let Err(e) = ctx.deactivate_session_lock() { + log::error!("Failed to deactivate session lock: {}", e); + } else { + log::info!("Session lock deactivated successfully"); + } + } + } + } + fn resolve_surface_target<'a>( ctx: &'a mut EventDispatchContext<'_>, target: &SurfaceTarget, @@ -770,6 +792,33 @@ impl Shell { self.control().popups() } + pub fn create_session_lock(&self, component: impl Into) -> Result { + self.create_session_lock_with_config(SessionLockBuilder::new(component)) + } + + pub fn create_session_lock_with_config( + &self, + builder: SessionLockBuilder, + ) -> Result { + let component = builder.component_name().to_string(); + if self.compilation_result.component(&component).is_none() { + return Err(Error::Domain(DomainError::Configuration { + message: format!( + "Component '{}' not found in compilation result", + component + ), + })); + } + + if !self.inner.borrow().is_session_lock_available() { + return Err(Error::ProtocolNotAvailable { + protocol: "ext-session-lock-v1".to_string(), + }); + } + + Ok(builder.build(Rc::downgrade(&self.inner), self.command_sender.clone())) + } + /// Returns the names of all registered surfaces pub fn surface_names(&self) -> Vec<&str> { self.registry.surface_names() diff --git a/crates/composition/src/system.rs b/crates/composition/src/system.rs index f91c8eb..1ceaa78 100644 --- a/crates/composition/src/system.rs +++ b/crates/composition/src/system.rs @@ -86,9 +86,14 @@ pub enum SurfaceCommand { }, } +pub enum SessionLockCommand { + Deactivate, +} + pub enum ShellCommand { Popup(PopupCommand), Surface(SurfaceCommand), + SessionLock(SessionLockCommand), Render, } @@ -748,6 +753,13 @@ impl EventDispatchContext<'_> { Ok(()) } + /// Deactivates the session lock + pub(crate) fn deactivate_session_lock(&mut self) -> Result<()> { + self.app_state + .deactivate_session_lock() + .map_err(Error::Adapter) + } + /// Returns the compilation result if available #[must_use] pub fn compilation_result(&self) -> Option> { diff --git a/crates/domain/src/prelude.rs b/crates/domain/src/prelude.rs index df891b5..efc442a 100644 --- a/crates/domain/src/prelude.rs +++ b/crates/domain/src/prelude.rs @@ -14,6 +14,8 @@ pub use crate::value_objects::dimensions::{PopupDimensions, SurfaceDimension}; pub use crate::value_objects::handle::{Handle, OutputHandle, PopupHandle, SurfaceHandle}; pub use crate::value_objects::keyboard_interactivity::KeyboardInteractivity; pub use crate::value_objects::layer::Layer; +pub use crate::value_objects::lock_config::LockConfig; +pub use crate::value_objects::lock_state::LockState; pub use crate::value_objects::margins::Margins; pub use crate::value_objects::output_info::{OutputGeometry, OutputInfo}; pub use crate::value_objects::output_policy::OutputPolicy; diff --git a/crates/domain/src/value_objects/lock_config.rs b/crates/domain/src/value_objects/lock_config.rs new file mode 100644 index 0000000..8a048a0 --- /dev/null +++ b/crates/domain/src/value_objects/lock_config.rs @@ -0,0 +1,40 @@ +use crate::dimensions::ScaleFactor; +use crate::errors::{DomainError, Result}; +use crate::value_objects::margins::Margins; +use crate::value_objects::output_policy::OutputPolicy; + +#[derive(Debug, Clone)] +pub struct LockConfig { + pub scale_factor: ScaleFactor, + pub margin: Margins, + pub namespace: String, + pub output_policy: OutputPolicy, +} + +impl LockConfig { + #[must_use] + pub fn new() -> Self { + Self { + scale_factor: ScaleFactor::default(), + margin: Margins::default(), + namespace: "layer-shika-lock".to_string(), + output_policy: OutputPolicy::AllOutputs, + } + } + + pub fn validate(&self) -> Result<()> { + let factor = self.scale_factor.value(); + if factor <= 0.0 || !factor.is_finite() { + return Err(DomainError::InvalidInput { + message: format!("Lock scale factor must be positive and finite, got {factor}"), + }); + } + Ok(()) + } +} + +impl Default for LockConfig { + fn default() -> Self { + Self::new() + } +} diff --git a/crates/domain/src/value_objects/lock_state.rs b/crates/domain/src/value_objects/lock_state.rs new file mode 100644 index 0000000..3aad546 --- /dev/null +++ b/crates/domain/src/value_objects/lock_state.rs @@ -0,0 +1,24 @@ +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum LockState { + Inactive, + Locking, + Locked, + Unlocking, +} + +impl LockState { + #[must_use] + pub const fn can_activate(self) -> bool { + matches!(self, Self::Inactive) + } + + #[must_use] + pub const fn can_deactivate(self) -> bool { + matches!(self, Self::Locked | Self::Locking) + } + + #[must_use] + pub const fn is_transitioning(self) -> bool { + matches!(self, Self::Locking | Self::Unlocking) + } +} diff --git a/crates/domain/src/value_objects/mod.rs b/crates/domain/src/value_objects/mod.rs index f7da72f..67d92f8 100644 --- a/crates/domain/src/value_objects/mod.rs +++ b/crates/domain/src/value_objects/mod.rs @@ -4,6 +4,8 @@ pub mod dimensions; pub mod handle; pub mod keyboard_interactivity; pub mod layer; +pub mod lock_config; +pub mod lock_state; pub mod margins; pub mod output_handle; pub mod output_info;