feat: add session lock support

This commit is contained in:
drendog 2025-12-28 22:01:13 +01:00
parent f4490ba9a1
commit 91455060c7
Signed by: dwenya
GPG key ID: 8DD77074645332D0
23 changed files with 1893 additions and 135 deletions

View file

@ -2,7 +2,7 @@
<div align="center">
<h1 style="font-size: 5em;">🦌</h1>
<p><i>"A cute layer of abstraction where Slint UIs grow antlers and become cute Wayland shells."</i></p>
<p><i>"A cute library where Slint UIs grow antlers and become cute Wayland shells."</i></p>
<p><b><a href="https://codeberg.org/waydeer/layer-shika">Main repo</a> | <a href="https://git.dren.dog/waydeer/layer-shika">Mirror</a> | <a href="https://github.com/waydeerwm/layer-shika">Temp mirror (github)</a></b></p>
</div>
@ -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.

View file

@ -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<WlOutput, ()> 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<wl_pointer::ButtonState>,
) {
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<WlPointer, ()> for AppState {
fn event(
state: &mut Self,
@ -203,76 +339,36 @@ impl Dispatch<WlPointer, ()> 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<WpFractionalScaleV1, ()> 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<WlRegistry, GlobalListContents> for AppState {
let output = registry.bind::<WlOutput, _, _>(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<WlRegistry, GlobalListContents> 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<WlRegistry, GlobalListContents> 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<WlRegistry, GlobalListContents> for AppState {
}
}
impl Dispatch<ExtSessionLockV1, ()> for AppState {
fn event(
state: &mut Self,
_proxy: &ExtSessionLockV1,
event: ext_session_lock_v1::Event,
_data: &(),
_conn: &Connection,
_queue_handle: &QueueHandle<Self>,
) {
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<ExtSessionLockSurfaceV1, ()> for AppState {
fn event(
state: &mut Self,
lock_surface: &ExtSessionLockSurfaceV1,
event: ext_session_lock_surface_v1::Event,
_data: &(),
_conn: &Connection,
_queue_handle: &QueueHandle<Self>,
) {
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, ()),

View file

@ -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<SharedString> {
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))
}

View file

@ -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<XdgWmBase>,
pub session_lock_manager: Option<ExtSessionLockManagerV1>,
pub fractional_scale_manager: Option<WpFractionalScaleManagerV1>,
pub viewporter: Option<WpViewporter>,
pub render_context_manager: Rc<RenderContextManager>,
@ -85,6 +87,10 @@ impl GlobalContext {
.bind::<XdgWmBase, _, _>(queue_handle, 1..=6, ())
.ok();
let session_lock_manager = global_list
.bind::<ExtSessionLockManagerV1, _, _>(queue_handle, 1..=1, ())
.ok();
let fractional_scale_manager = global_list
.bind::<WpFractionalScaleManagerV1, _, _>(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,

View file

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

View file

@ -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<dyn Fn(&[Value]) -> 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<LockState>;
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;

View file

@ -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<WpFractionalScaleManagerV1>,
viewporter: Option<WpViewporter>,
render_factory: Rc<RenderContextFactory>,
}
impl SessionLockContext {
#[must_use]
pub fn new(
compositor: WlCompositor,
lock_manager: ExtSessionLockManagerV1,
seat: WlSeat,
fractional_scale_manager: Option<WpFractionalScaleManagerV1>,
viewporter: Option<WpViewporter>,
render_factory: Rc<RenderContextFactory>,
) -> 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<RenderContextFactory> {
&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<AppState>,
}

View file

@ -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<dyn Fn(&[Value]) -> Value>;
#[derive(Clone)]
pub(crate) struct LockCallback {
name: String,
handler: LockCallbackHandler,
}
impl LockCallback {
pub fn new(name: impl Into<String>, 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<FemtoVGWindow>,
component: Option<ComponentState>,
scale_factor: f32,
has_fractional_scale: bool,
}
struct LockConfigureContext {
scale_factor: f32,
component_definition: ComponentDefinition,
compilation_result: Option<Rc<CompilationResult>>,
platform: Rc<CustomSlintPlatform>,
callbacks: Vec<LockCallback>,
}
impl ActiveLockSurface {
fn new(surface: LockSurface, window: Rc<FemtoVGWindow>) -> 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<FemtoVGWindow> {
Rc::clone(&self.window)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum LockScalingMode {
FractionalWithViewport,
FractionalOnly,
Integer,
}
pub struct SessionLockManager {
context: Rc<SessionLockContext>,
session_lock: Option<ExtSessionLockV1>,
lock_surfaces: HashMap<ObjectId, ActiveLockSurface>,
state: LockState,
config: LockConfig,
component_definition: ComponentDefinition,
compilation_result: Option<Rc<CompilationResult>>,
platform: Rc<CustomSlintPlatform>,
callbacks: Vec<LockCallback>,
active_pointer_surface_id: Option<ObjectId>,
keyboard_focus_surface_id: Option<ObjectId>,
current_pointer_position: LogicalPosition,
accumulated_axis_x: f32,
accumulated_axis_y: f32,
}
impl SessionLockManager {
#[must_use]
pub fn new(
context: Rc<SessionLockContext>,
component_definition: ComponentDefinition,
compilation_result: Option<Rc<CompilationResult>>,
platform: Rc<CustomSlintPlatform>,
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<Item = WlOutput>,
queue_handle: &QueueHandle<AppState>,
) -> 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(&params);
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<AppState>,
) -> 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(&params);
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<wl_pointer::ButtonState>,
) -> 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<Rc<FemtoVGWindow>> {
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)
}
}

View file

@ -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<WlSurface>,
session_surface: Rc<ExtSessionLockSurfaceV1>,
fractional_scale: Option<Rc<WpFractionalScaleV1>>,
viewport: Option<Rc<WpViewport>>,
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<WpFractionalScaleV1>> {
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();
}
}

View file

@ -0,0 +1,3 @@
pub mod lock_context;
pub mod lock_manager;
pub mod lock_surface;

View file

@ -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<AppState>,
) -> Result<AppState> {
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<AppState>,
) -> Result<AppState> {
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<LockState> {
self.state.current_lock_state()
}
fn register_session_lock_callback(
&mut self,
callback_name: &str,
handler: Rc<dyn Fn(&[slint_interpreter::Value]) -> slint_interpreter::Value>,
) {
self.state
.register_session_lock_callback(callback_name, handler);
}
fn app_state(&self) -> &AppState {
WaylandShellSystem::app_state(self)
}

View file

@ -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<dyn Fn(&[Value]) -> Value>;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ShellSurfaceKey {
@ -35,6 +45,9 @@ impl ShellSurfaceKey {
}
pub struct AppState {
global_context: Option<Rc<GlobalContext>>,
known_outputs: Vec<WlOutput>,
slint_platform: Option<Rc<CustomSlintPlatform>>,
output_registry: OutputRegistry,
output_mapping: OutputMapping,
surfaces: HashMap<ShellSurfaceKey, PerOutputSurface>,
@ -49,6 +62,8 @@ pub struct AppState {
keyboard_focus_key: Option<ShellSurfaceKey>,
keyboard_focus_surface_id: Option<ObjectId>,
keyboard_state: KeyboardState,
lock_manager: Option<SessionLockManager>,
lock_callbacks: Vec<LockCallback>,
}
impl AppState {
@ -58,6 +73,9 @@ impl AppState {
shared_serial: Rc<SharedPointerSerial>,
) -> 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<GlobalContext>) {
self.known_outputs.clone_from(&context.outputs);
self.global_context = Some(context);
}
pub fn set_slint_platform(&mut self, platform: Rc<CustomSlintPlatform>) {
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<LockState> {
self.lock_manager.as_ref().map(SessionLockManager::state)
}
pub fn register_session_lock_callback(
&mut self,
callback_name: impl Into<String>,
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<AppState>,
) -> 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<Rc<CompilationResult>>)> {
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<Rc<SessionLockContext>> {
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<WlOutput> {
self.known_outputs.clone()
}
pub fn handle_output_added_for_lock(
&mut self,
output: &WlOutput,
queue_handle: &wayland_client::QueueHandle<AppState>,
) -> 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);
}
}

View file

@ -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<SharedString> {
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))
}

View file

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

View file

@ -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,
}
}

View file

@ -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,
};

View file

@ -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<RefCell<dyn WaylandSystemOps>>,
component_name: String,
config: LockConfig,
state: Cell<LockState>,
command_sender: channel::Sender<ShellCommand>,
}
impl SessionLock {
pub(crate) fn new(
system: Weak<RefCell<dyn WaylandSystemOps>>,
component_name: String,
config: LockConfig,
command_sender: channel::Sender<ShellCommand>,
) -> 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<F, R>(&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<F, R>(&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<String>) -> Self {
Self {
component_name: component_name.into(),
config: LockConfig::default(),
}
}
#[must_use]
pub fn scale_factor(mut self, factor: impl TryInto<ScaleFactor, Error = DomainError>) -> Self {
self.config.scale_factor = factor.try_into().unwrap_or_default();
self
}
#[must_use]
pub fn margin(mut self, margin: impl Into<Margins>) -> Self {
self.config.margin = margin.into();
self
}
#[must_use]
pub fn namespace(mut self, namespace: impl Into<String>) -> 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<RefCell<dyn WaylandSystemOps>>,
command_sender: channel::Sender<ShellCommand>,
) -> SessionLock {
SessionLock::new(system, self.component_name, self.config, command_sender)
}
#[must_use]
pub fn component_name(&self) -> &str {
&self.component_name
}
}

View file

@ -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<ShellCommand>) -> 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<RefCell<dyn WaylandSystemOps>>,
) {
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<String>) -> Result<SessionLock> {
self.create_session_lock_with_config(SessionLockBuilder::new(component))
}
pub fn create_session_lock_with_config(
&self,
builder: SessionLockBuilder,
) -> Result<SessionLock> {
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()

View file

@ -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<Rc<CompilationResult>> {

View file

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

View file

@ -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()
}
}

View file

@ -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)
}
}

View file

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