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;