From 979a60171d4f6011fc7af660865280de13d9d8ed Mon Sep 17 00:00:00 2001 From: Ivan Molodetskikh Date: Fri, 13 Oct 2023 13:30:11 +0400 Subject: [PATCH] Implement ext-session-lock --- Cargo.lock | 2 +- Cargo.toml | 4 +- src/handlers/compositor.rs | 13 ++ src/handlers/mod.rs | 43 ++++- src/input.rs | 15 +- src/niri.rs | 310 +++++++++++++++++++++++++++++++++++-- 6 files changed, 367 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e40839ff3..1540a3aa9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2280,7 +2280,7 @@ checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" [[package]] name = "smithay" version = "0.3.0" -source = "git+https://github.com/Smithay/smithay.git#4e41ab0694c1c43324cc217dcf2a203cd240c0dc" +source = "git+https://github.com/YaLTeR/smithay.git?branch=session-lock#201cf65a8ec14d72505d287827ed03fb2ababf23" dependencies = [ "appendlist", "bitflags 2.4.0", diff --git a/Cargo.toml b/Cargo.toml index 6393f07ba..7114bfb83 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,7 +35,9 @@ xcursor = "0.3.4" zbus = { version = "3.14.1", optional = true } [dependencies.smithay] -git = "https://github.com/Smithay/smithay.git" +# git = "https://github.com/Smithay/smithay.git" +git = "https://github.com/YaLTeR/smithay.git" +branch = "session-lock" # path = "../smithay" default-features = false features = [ diff --git a/src/handlers/compositor.rs b/src/handlers/compositor.rs index d8d93d9c8..128d4b74a 100644 --- a/src/handlers/compositor.rs +++ b/src/handlers/compositor.rs @@ -160,6 +160,19 @@ impl CompositorHandler for State { // FIXME: granular redraws for cursors. self.niri.queue_redraw_all(); } + + // This might be a lock surface. + if self.niri.is_locked() { + for (output, state) in &self.niri.output_state { + if let Some(lock_surface) = &state.lock_surface { + if lock_surface.wl_surface() == surface { + self.niri.queue_redraw(output.clone()); + self.refresh_cursor_focus(); + break; + } + } + } + } } } diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index d14650c1c..81ef98064 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -13,10 +13,12 @@ use smithay::backend::renderer::ImportDma; use smithay::desktop::PopupKind; use smithay::input::pointer::CursorImageStatus; use smithay::input::{Seat, SeatHandler, SeatState}; +use smithay::output::Output; use smithay::reexports::wayland_server::protocol::wl_data_source::WlDataSource; +use smithay::reexports::wayland_server::protocol::wl_output::WlOutput; use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; use smithay::reexports::wayland_server::Resource; -use smithay::utils::{Logical, Rectangle}; +use smithay::utils::{Logical, Rectangle, Size}; use smithay::wayland::dmabuf::{DmabufGlobal, DmabufHandler, DmabufState, ImportError}; use smithay::wayland::input_method::{InputMethodHandler, PopupSurface}; use smithay::wayland::selection::data_device::{ @@ -28,13 +30,17 @@ use smithay::wayland::selection::primary_selection::{ }; use smithay::wayland::selection::wlr_data_control::{DataControlHandler, DataControlState}; use smithay::wayland::selection::{SelectionHandler, SelectionTarget}; +use smithay::wayland::session_lock::{ + LockSurface, SessionLockHandler, SessionLockManagerState, SessionLocker, +}; use smithay::{ delegate_data_control, delegate_data_device, delegate_dmabuf, delegate_input_method_manager, delegate_output, delegate_pointer_gestures, delegate_presentation, delegate_primary_selection, - delegate_seat, delegate_tablet_manager, delegate_text_input_manager, + delegate_seat, delegate_session_lock, delegate_tablet_manager, delegate_text_input_manager, delegate_virtual_keyboard_manager, }; +use crate::layout::output_size; use crate::niri::State; impl SeatHandler for State { @@ -174,3 +180,36 @@ impl DmabufHandler for State { } } delegate_dmabuf!(State); + +impl SessionLockHandler for State { + fn lock_state(&mut self) -> &mut SessionLockManagerState { + &mut self.niri.session_lock_state + } + + fn lock(&mut self, confirmation: SessionLocker) { + self.niri.lock(confirmation); + } + + fn unlock(&mut self) { + self.niri.unlock(); + } + + fn new_surface(&mut self, surface: LockSurface, output: WlOutput) { + let Some(output) = Output::from_resource(&output) else { + error!("no Output matching WlOutput"); + return; + }; + + configure_lock_surface(&surface, &output); + self.niri.new_lock_surface(surface, &output); + } +} +delegate_session_lock!(State); + +pub fn configure_lock_surface(surface: &LockSurface, output: &Output) { + surface.with_pending_state(|states| { + let size = output_size(output); + states.size = Some(Size::from((size.w as u32, size.h as u32))); + }); + surface.send_configure(); +} diff --git a/src/input.rs b/src/input.rs index 6b6baa340..b672da46b 100644 --- a/src/input.rs +++ b/src/input.rs @@ -136,7 +136,7 @@ impl State { let serial = SERIAL_COUNTER.next_serial(); let time = Event::time_msec(&event); - let action = self.niri.seat.get_keyboard().unwrap().input( + let mut action = self.niri.seat.get_keyboard().unwrap().input( self, event.key_code(), event.state(), @@ -152,6 +152,19 @@ impl State { }, ); + // Filter actions when the session is locked. + if self.niri.is_locked() { + match action { + Some( + Action::Quit + | Action::ChangeVt(_) + | Action::Suspend + | Action::PowerOffMonitors, + ) => (), + _ => action = None, + } + } + if let Some(action) = action { match action { Action::None => unreachable!(), diff --git a/src/niri.rs b/src/niri.rs index a57142869..51c6c91fb 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -11,6 +11,7 @@ use _server_decoration::server::org_kde_kwin_server_decoration_manager::Mode as use anyhow::Context; use image::ImageFormat; use smithay::backend::allocator::Fourcc; +use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement}; use smithay::backend::renderer::element::surface::{ render_elements_from_surface_tree, WaylandSurfaceRenderElement, }; @@ -25,7 +26,7 @@ use smithay::desktop::utils::{ bbox_from_surface_tree, output_update, send_dmabuf_feedback_surface_tree, send_frames_surface_tree, surface_presentation_feedback_flags_from_states, surface_primary_scanout_output, take_presentation_feedback_surface_tree, - update_surface_primary_scanout_output, OutputPresentationFeedback, + under_from_surface_tree, update_surface_primary_scanout_output, OutputPresentationFeedback, }; use smithay::desktop::{layer_map_for_output, PopupManager, Space, Window, WindowSurfaceType}; use smithay::input::keyboard::XkbConfig; @@ -62,6 +63,7 @@ use smithay::wayland::selection::primary_selection::{ set_primary_selection, PrimarySelectionState, }; use smithay::wayland::selection::wlr_data_control::DataControlState; +use smithay::wayland::session_lock::{LockSurface, SessionLockManagerState, SessionLocker}; use smithay::wayland::shell::kde::decoration::KdeDecorationState; use smithay::wayland::shell::wlr_layer::{Layer, WlrLayerShellState}; use smithay::wayland::shell::xdg::decoration::XdgDecorationState; @@ -72,7 +74,7 @@ use smithay::wayland::tablet_manager::TabletManagerState; use smithay::wayland::text_input::TextInputManagerState; use smithay::wayland::virtual_keyboard::VirtualKeyboardManagerState; -use crate::backend::{Backend, Tty, Winit}; +use crate::backend::{Backend, RenderResult, Tty, Winit}; use crate::config::Config; use crate::cursor::Cursor; #[cfg(feature = "dbus")] @@ -80,11 +82,13 @@ use crate::dbus::gnome_shell_screenshot::{NiriToScreenshot, ScreenshotToNiri}; #[cfg(feature = "xdp-gnome-screencast")] use crate::dbus::mutter_screen_cast::{self, ScreenCastToNiri}; use crate::frame_clock::FrameClock; +use crate::handlers::configure_lock_surface; use crate::layout::{output_size, Layout, MonitorRenderElement}; use crate::pw_utils::{Cast, PipeWire}; use crate::utils::{center, get_monotonic_time, make_screenshot_path}; pub const CLEAR_COLOR: [f32; 4] = [0.2, 0.2, 0.2, 1.]; +pub const CLEAR_COLOR_LOCKED: [f32; 4] = [0.3, 0.1, 0.1, 1.]; pub struct Niri { pub config: Rc>, @@ -117,6 +121,7 @@ pub struct Niri { pub xdg_decoration_state: XdgDecorationState, pub kde_decoration_state: KdeDecorationState, pub layer_shell_state: WlrLayerShellState, + pub session_lock_state: SessionLockManagerState, pub shm_state: ShmState, pub output_manager_state: OutputManagerState, pub seat_state: SeatState, @@ -137,6 +142,8 @@ pub struct Niri { pub cursor_image: CursorImageStatus, pub dnd_icon: Option, + pub lock_state: LockState, + #[cfg(feature = "dbus")] pub dbus: Option, #[cfg(feature = "dbus")] @@ -162,6 +169,9 @@ pub struct OutputState { /// If there are no commits, then we won't have a timer running, so the estimated sequence will /// not increase. pub current_estimated_sequence: Option, + pub lock_render_state: LockRenderState, + pub lock_surface: Option, + pub lock_color_buffer: SolidColorBuffer, } #[derive(Default)] @@ -179,6 +189,22 @@ pub enum RedrawState { WaitingForEstimatedVBlankAndQueued((RegistrationToken, Idle<'static>)), } +#[derive(Default)] +pub enum LockState { + #[default] + Unlocked, + Locking(SessionLocker), + Locked, +} + +#[derive(PartialEq, Eq)] +pub enum LockRenderState { + /// The output displays a normal session frame. + Unlocked, + /// The output displays a locked frame. + Locked, +} + // Not related to the one in Smithay. // // This state keeps track of when a surface last received a frame callback. @@ -258,18 +284,28 @@ impl State { self.niri.queue_redraw_all(); } + pub fn refresh_cursor_focus(&mut self) { + let pointer = &self.niri.seat.get_pointer().unwrap(); + self.move_cursor(pointer.current_location()); + } + pub fn move_cursor_to_output(&mut self, output: &Output) { let geo = self.niri.global_space.output_geometry(output).unwrap(); self.move_cursor(center(geo).to_f64()); } pub fn update_focus(&mut self) { - let focus = self.niri.layer_surface_focus().or_else(|| { - self.niri - .layout - .focus() - .map(|win| win.toplevel().wl_surface().clone()) - }); + let focus = if self.niri.is_locked() { + self.niri.lock_surface_focus() + } else { + self.niri.layer_surface_focus().or_else(|| { + self.niri + .layout + .focus() + .map(|win| win.toplevel().wl_surface().clone()) + }) + }; + let keyboard = self.niri.seat.get_keyboard().unwrap(); if keyboard.current_focus() != focus { keyboard.set_focus(self, focus, SERIAL_COUNTER.next_serial()); @@ -429,6 +465,8 @@ impl Niri { }, ); let layer_shell_state = WlrLayerShellState::new::(&display_handle); + let session_lock_state = + SessionLockManagerState::new::(&display_handle, |_| true); let shm_state = ShmState::new::(&display_handle, vec![]); let output_manager_state = OutputManagerState::new_with_xdg_output::(&display_handle); @@ -520,6 +558,7 @@ impl Niri { xdg_decoration_state, kde_decoration_state, layer_shell_state, + session_lock_state, text_input_state, input_method_state, virtual_keyboard_state, @@ -539,6 +578,8 @@ impl Niri { cursor_image: CursorImageStatus::default_named(), dnd_icon: None, + lock_state: LockState::Unlocked, + #[cfg(feature = "dbus")] dbus: None, #[cfg(feature = "dbus")] @@ -636,12 +677,22 @@ impl Niri { self.layout.add_output(output.clone()); output.change_current_state(None, None, None, Some(position)); + let lock_render_state = if self.is_locked() { + // We haven't rendered anything yet so it's as good as locked. + LockRenderState::Locked + } else { + LockRenderState::Unlocked + }; + let state = OutputState { global, redraw_state: RedrawState::Idle, unfinished_animations_remain: false, frame_clock: FrameClock::new(refresh_interval), current_estimated_sequence: None, + lock_render_state, + lock_surface: None, + lock_color_buffer: SolidColorBuffer::new(size, CLEAR_COLOR_LOCKED), }; let rv = self.output_state.insert(output.clone(), state); assert!(rv.is_none(), "output was already tracked"); @@ -684,11 +735,41 @@ impl Niri { }, ) .unwrap(); + + match mem::take(&mut self.lock_state) { + LockState::Locking(confirmation) => { + // We're locking and an output was removed, check if the requirements are now met. + let all_locked = self + .output_state + .values() + .all(|state| state.lock_render_state == LockRenderState::Locked); + + if all_locked { + confirmation.lock(); + self.lock_state = LockState::Locked; + } else { + // Still waiting. + self.lock_state = LockState::Locking(confirmation); + } + } + lock_state => self.lock_state = lock_state, + } } pub fn output_resized(&mut self, output: Output) { layer_map_for_output(&output).arrange(); self.layout.update_output_size(&output); + + let is_locked = self.is_locked(); + if let Some(state) = self.output_state.get_mut(&output) { + state.lock_color_buffer.resize(output_size(&output)); + if is_locked { + if let Some(lock_surface) = &state.lock_surface { + configure_lock_surface(lock_surface, &output); + } + } + } + self.queue_redraw(output); } @@ -726,6 +807,10 @@ impl Niri { } pub fn window_under_cursor(&self) -> Option<&Window> { + if self.is_locked() { + return None; + } + let pos = self.seat.get_pointer().unwrap().current_location(); let (output, pos_within_output) = self.output_under(pos)?; let (window, _loc) = self.layout.window_under(output, pos_within_output)?; @@ -742,6 +827,20 @@ impl Niri { pos: Point, ) -> Option<(WlSurface, Point)> { let (output, pos_within_output) = self.output_under(pos)?; + + if self.is_locked() { + let state = self.output_state.get(output)?; + let surface = state.lock_surface.as_ref()?; + // We put lock surfaces at (0, 0). + let point = pos_within_output; + return under_from_surface_tree( + surface.wl_surface(), + point, + (0, 0), + WindowSurfaceType::ALL, + ); + } + let (window, win_pos_within_output) = self.layout.window_under(output, pos_within_output)?; @@ -838,6 +937,17 @@ impl Niri { .or_else(|| self.global_space.outputs().next()) } + fn lock_surface_focus(&self) -> Option { + let output_under_cursor = self.output_under_cursor(); + let output = output_under_cursor + .as_ref() + .or_else(|| self.layout.active_output()) + .or_else(|| self.global_space.outputs().next())?; + + let state = self.output_state.get(output)?; + state.lock_surface.as_ref().map(|s| s.wl_surface()).cloned() + } + fn layer_surface_focus(&self) -> Option { let output = self.layout.active_output()?; let layers = layer_map_for_output(output); @@ -1052,16 +1162,45 @@ impl Niri { let output_scale = Scale::from(output.current_scale().fractional_scale()); - // Get monitor elements. - let mon = self.layout.monitor_for_output(output).unwrap(); - let monitor_elements = mon.render_elements(renderer); - // The pointer goes on the top. let mut elements = vec![]; if include_pointer { elements = self.pointer_element(renderer, output); } + // If the session is locked, draw the lock surface. + if self.is_locked() { + let state = self.output_state.get_mut(output).unwrap(); + if let Some(surface) = state.lock_surface.as_ref() { + elements.extend(render_elements_from_surface_tree( + renderer, + surface.wl_surface(), + (0, 0), + output_scale, + 1., + Kind::Unspecified, + )); + } + + // Draw the solid color background. + elements.push( + SolidColorRenderElement::from_buffer( + &state.lock_color_buffer, + (0, 0), + output_scale, + 1., + Kind::Unspecified, + ) + .into(), + ); + + return elements; + } + + // Get monitor elements. + let mon = self.layout.monitor_for_output(output).unwrap(); + let monitor_elements = mon.render_elements(renderer); + // Get layer-shell elements. let layer_map = layer_map_for_output(output); let mut extend_from_layer = |elements: &mut Vec>, @@ -1105,13 +1244,17 @@ impl Niri { fn redraw(&mut self, backend: &mut Backend, output: &Output) { let _span = tracy_client::span!("Niri::redraw"); + let monitors_active = self.monitors_active; + let state = self.output_state.get_mut(output).unwrap(); assert!(matches!( state.redraw_state, RedrawState::Queued(_) | RedrawState::WaitingForEstimatedVBlankAndQueued(_) )); + // FIXME: make this not cursed. let mut reset = || { + let state = self.output_state.get_mut(output).unwrap(); state.redraw_state = if let RedrawState::WaitingForEstimatedVBlankAndQueued((token, _)) = state.redraw_state @@ -1119,10 +1262,17 @@ impl Niri { RedrawState::WaitingForEstimatedVBlank(token) } else { RedrawState::Idle - } + }; + + if matches!(self.lock_state, LockState::Locking { .. }) + && state.lock_render_state == LockRenderState::Unlocked + { + // We needed to redraw this output for locking and failed. + self.unlock(); + } }; - if !self.monitors_active { + if !monitors_active { reset(); return; } @@ -1137,6 +1287,7 @@ impl Niri { return; }; + let state = self.output_state.get_mut(output).unwrap(); let presentation_time = state.frame_clock.next_presentation_time(); // Update from the config and advance the animations. @@ -1151,7 +1302,48 @@ impl Niri { let elements = self.render(renderer, output, true); // Hand it over to the backend. - backend.render(self, output, &elements, presentation_time); + let res = backend.render(self, output, &elements, presentation_time); + + // Update the lock render state on successful render. + let is_locked = self.is_locked(); + let state = self.output_state.get_mut(output).unwrap(); + if res != RenderResult::Error { + state.lock_render_state = if is_locked { + LockRenderState::Locked + } else { + LockRenderState::Unlocked + }; + } + + // If we're in process of locking the session, check if the requirements were met. + match mem::take(&mut self.lock_state) { + LockState::Locking(confirmation) => { + if res == RenderResult::Error { + if state.lock_render_state == LockRenderState::Unlocked { + // We needed to render a locked frame on this output but failed. + self.unlock(); + } else { + // Rendering failed but this output is already locked, so it's fine. + self.lock_state = LockState::Locking(confirmation); + } + } else { + // Rendering succeeded, check if this was the last output. + let all_locked = self + .output_state + .values() + .all(|state| state.lock_render_state == LockRenderState::Locked); + + if all_locked { + confirmation.lock(); + self.lock_state = LockState::Locked; + } else { + // Still waiting. + self.lock_state = LockState::Locking(confirmation); + } + } + } + lock_state => self.lock_state = lock_state, + } // Send the frame callbacks. // @@ -1254,6 +1446,24 @@ impl Niri { ); }); } + + if let Some(surface) = &self.output_state[output].lock_surface { + with_surface_tree_downward( + surface.wl_surface(), + (), + |_, _, _| TraversalAction::DoChildren(()), + |surface, states, _| { + update_surface_primary_scanout_output( + surface, + output, + states, + render_element_states, + default_primary_scanout_output_compare, + ); + }, + |_, _, _| true, + ); + } } pub fn send_dmabuf_feedbacks(&self, output: &Output, feedback: &DmabufFeedback) { @@ -1270,6 +1480,15 @@ impl Niri { surface.send_dmabuf_feedback(output, |_, _| Some(output.clone()), |_, _| feedback); } + if let Some(surface) = &self.output_state[output].lock_surface { + send_dmabuf_feedback_surface_tree( + surface.wl_surface(), + output, + |_, _| Some(output.clone()), + |_, _| feedback, + ); + } + if let Some(surface) = &self.dnd_icon { send_dmabuf_feedback_surface_tree( surface, @@ -1341,6 +1560,16 @@ impl Niri { surface.send_frame(output, frame_callback_time, None, should_send); } + if let Some(surface) = &self.output_state[output].lock_surface { + send_frames_surface_tree( + surface.wl_surface(), + output, + frame_callback_time, + None, + should_send, + ); + } + if let Some(surface) = &self.dnd_icon { send_frames_surface_tree(surface, output, frame_callback_time, None, should_send); } @@ -1399,6 +1628,17 @@ impl Niri { ); } + if let Some(surface) = &self.output_state[output].lock_surface { + take_presentation_feedback_surface_tree( + surface.wl_surface(), + &mut feedback, + surface_primary_scanout_output, + |surface, _| { + surface_presentation_feedback_flags_from_states(surface, render_element_states) + }, + ); + } + feedback } @@ -1652,6 +1892,45 @@ impl Niri { Ok(()) } + + pub fn is_locked(&self) -> bool { + !matches!(self.lock_state, LockState::Unlocked) + } + + pub fn lock(&mut self, confirmation: SessionLocker) { + info!("locking session"); + + self.lock_state = LockState::Locking(confirmation); + self.queue_redraw_all(); + self.event_loop + .insert_idle(|state| state.refresh_cursor_focus()); + } + + pub fn unlock(&mut self) { + info!("unlocking session"); + + self.lock_state = LockState::Unlocked; + for output_state in self.output_state.values_mut() { + output_state.lock_surface = None; + } + self.queue_redraw_all(); + self.event_loop + .insert_idle(|state| state.refresh_cursor_focus()); + } + + pub fn new_lock_surface(&mut self, surface: LockSurface, output: &Output) { + if !self.is_locked() { + error!("tried to add a lock surface on an unlocked session"); + return; + } + + let Some(output_state) = self.output_state.get_mut(output) else { + error!("missing output state"); + return; + }; + + output_state.lock_surface = Some(surface); + } } render_elements! { @@ -1660,6 +1939,7 @@ render_elements! { Monitor = MonitorRenderElement, Wayland = WaylandSurfaceRenderElement, DefaultPointer = TextureRenderElement<::TextureId>, + SolidColor = SolidColorRenderElement, } #[derive(Default)]