srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/wayland/src/udev
diff options
context:
space:
mode:
Diffstat (limited to 'crates/wayland/src/udev')
-rw-r--r--crates/wayland/src/udev/capture.rs129
-rw-r--r--crates/wayland/src/udev/mod.rs26
-rw-r--r--crates/wayland/src/udev/platform.rs111
-rw-r--r--crates/wayland/src/udev/render.rs84
-rw-r--r--crates/wayland/src/udev/session.rs35
5 files changed, 374 insertions, 11 deletions
diff --git a/crates/wayland/src/udev/capture.rs b/crates/wayland/src/udev/capture.rs
new file mode 100644
index 0000000..0f87d27
--- /dev/null
+++ b/crates/wayland/src/udev/capture.rs
@@ -0,0 +1,129 @@
+//! Off-screen render of a workspace that isn't necessarily the one
+//! currently on screen - `srd capture workspace <id> <path>`, drained
+//! from `WindowManager::drain_capture_requests` on every poll. See
+//! `srdwm_core::CaptureRequest`'s own doc comment for why this exists at
+//! all: `wlr-screencopy` (`crates/wayland/src/screencopy.rs`, and `grim`)
+//! can only ever see what an output is actually presenting, and a
+//! workspace switcher's thumbnail needs exactly the opposite - a
+//! workspace that, most of the time, is *not* the one presented.
+//!
+//! Deliberately simple, not a small reimplementation of
+//! `render_udev_frame`: only window content is drawn, no borders,
+//! shadows, titlebars, cursor or layer-shell surfaces - every consumer
+//! this was built for (a workspace-switcher tile) draws those tiny, where
+//! that detail is imperceptible, and skipping them keeps this from needing
+//! to duplicate that function's animation/occlusion bookkeeping. Always
+//! renders at the target monitor's native resolution and downscales
+//! afterward if a smaller size was requested, rather than trying to get
+//! smithay's fractional-output-scale rendering path exactly right for a
+//! target with no real `Output` behind it.
+
+use super::*;
+use smithay::backend::allocator::Fourcc;
+use smithay::backend::renderer::element::surface::{render_elements_from_surface_tree, WaylandSurfaceRenderElement};
+use smithay::backend::renderer::element::Kind;
+use smithay::backend::renderer::{Bind, ExportMem, Offscreen};
+use smithay::utils::{Buffer as BufferCoord, Transform};
+
+impl CompState {
+ /// Services every capture request queued since the last poll. Takes
+ /// the `Vec` by value for the same reason `screencopy::service_pending`
+ /// does: the renderer this needs lives behind `self.udev`'s own
+ /// mutable borrow, so the request list has to be lifted out of
+ /// `self.wm` before that borrow starts.
+ pub(crate) fn service_capture_requests(&mut self, requests: Vec<srdwm_core::CaptureRequest>) {
+ for req in requests {
+ if let Err(e) = self.capture_workspace(&req) {
+ log::warn!("capture: workspace {} -> {}: {e}", req.workspace, req.path);
+ }
+ }
+ }
+
+ fn capture_workspace(&mut self, req: &srdwm_core::CaptureRequest) -> Result<(), String> {
+ // The monitor a freshly-placed window on this workspace would land
+ // on: workspaces aren't per-monitor in this compositor (a single
+ // `current_workspace` is shared by every screen - see
+ // `WindowManager`'s own field doc comment), so there's no single
+ // "this workspace's monitor" to ask for; the primary one is the
+ // same reasonable default `arrange_workspace` itself falls back to.
+ let (origin, native): ((i32, i32), (u32, u32)) = {
+ let wm = self.wm.borrow();
+ let monitor = wm.monitors().iter().find(|m| m.primary).or_else(|| wm.monitors().first()).ok_or("no monitor to capture from")?;
+ ((monitor.full_geometry.x, monitor.full_geometry.y), (monitor.full_geometry.width, monitor.full_geometry.height))
+ };
+ if native.0 == 0 || native.1 == 0 {
+ return Err("monitor has zero size".to_string());
+ }
+
+ let ids = self.wm.borrow().window_ids_on_workspace_front_to_back(req.workspace);
+ let mut elements: Vec<WaylandSurfaceRenderElement<PixmanRenderer>> = Vec::new();
+ let Some(udev) = self.udev.as_mut() else { return Err("no udev backend".to_string()) };
+ for id in ids {
+ let Some(w) = self.id_to_window.get(&id) else { continue };
+ let Some(surface) = crate::input::dwindow_wl_surface(w) else { continue };
+ let Some(geom) = self.wm.borrow().window(id).map(|w| w.geometry) else { continue };
+ let loc = (geom.x - origin.0, geom.y - origin.1);
+ elements.extend(render_elements_from_surface_tree(&mut udev.renderer, &surface, loc, 1.0, 1.0, Kind::Unspecified));
+ }
+
+ let (nw, nh) = (native.0 as i32, native.1 as i32);
+ let mut target = udev.renderer.create_buffer(Fourcc::Xrgb8888, (nw, nh).into()).map_err(|e| format!("create_buffer: {e}"))?;
+ let mut framebuffer = udev.renderer.bind(&mut target).map_err(|e| format!("bind: {e}"))?;
+ let mut tracker = OutputDamageTracker::new((nw, nh), 1.0, Transform::Normal);
+ tracker
+ .render_output(&mut udev.renderer, &mut framebuffer, 0, &elements, [0.0, 0.0, 0.0, 1.0])
+ .map_err(|e| format!("render_output: {e:?}"))?;
+
+ let region: Rectangle<i32, BufferCoord> = Rectangle::new((0, 0).into(), (nw, nh).into());
+ let mapping = udev.renderer.copy_framebuffer(&framebuffer, region, Fourcc::Xrgb8888).map_err(|e| format!("copy_framebuffer: {e}"))?;
+ let pixels = udev.renderer.map_texture(&mapping).map_err(|e| format!("map_texture: {e}"))?;
+
+ write_ppm(pixels, native, req.size, &req.path)
+ }
+}
+
+/// `pixels` is `Xrgb8888` - 4 bytes per pixel, little-endian, so byte
+/// order in memory is B, G, R, X. PPM (`P6`) wants tightly-packed R, G, B
+/// with no pad byte, hence the reorder rather than a straight `memcpy`.
+/// Downscales with plain nearest-neighbor sampling when `target` is
+/// smaller than `native` - a thumbnail has no need for anything more
+/// expensive, and this avoids pulling in an image-scaling crate for one
+/// call site.
+fn write_ppm(pixels: &[u8], native: (u32, u32), target: Option<(u32, u32)>, path: &str) -> Result<(), String> {
+ let (nw, nh) = native;
+ let (tw, th) = target.unwrap_or(native);
+ if tw == 0 || th == 0 {
+ return Err("requested capture size is zero".to_string());
+ }
+ let src_stride = nw as usize * 4;
+ let needed = src_stride * nh as usize;
+ if pixels.len() < needed {
+ return Err(format!("readback produced {} bytes, need {needed}", pixels.len()));
+ }
+
+ let mut rgb = Vec::with_capacity(tw as usize * th as usize * 3);
+ for ty in 0..th {
+ // `.min(nh - 1)`/`.min(nw - 1)`: guards the last row/column of a
+ // downscale from ever reading one pixel past the source when an
+ // integer ratio rounds up, not a real expectation of overflow.
+ let sy = (ty as u64 * nh as u64 / th as u64).min(nh as u64 - 1) as usize;
+ for tx in 0..tw {
+ let sx = (tx as u64 * nw as u64 / tw as u64).min(nw as u64 - 1) as usize;
+ let i = sy * src_stride + sx * 4;
+ rgb.push(pixels[i + 2]); // R
+ rgb.push(pixels[i + 1]); // G
+ rgb.push(pixels[i]); // B
+ }
+ }
+
+ let mut out = format!("P6\n{tw} {th}\n255\n").into_bytes();
+ out.extend_from_slice(&rgb);
+ // Written to a `.tmp` sibling and renamed into place: a reader (AGS's
+ // wsPreview poller) racing a partial write is exactly the kind of
+ // flicker/corruption a debounced, event-driven cache is supposed to
+ // avoid - `rename` within the same directory is atomic, a plain
+ // `write` never is.
+ let tmp = format!("{path}.tmp");
+ std::fs::write(&tmp, &out).map_err(|e| format!("write {tmp}: {e}"))?;
+ std::fs::rename(&tmp, path).map_err(|e| format!("rename to {path}: {e}"))
+}
diff --git a/crates/wayland/src/udev/mod.rs b/crates/wayland/src/udev/mod.rs
index 2d9f9ff..501e3fc 100644
--- a/crates/wayland/src/udev/mod.rs
+++ b/crates/wayland/src/udev/mod.rs
@@ -33,8 +33,9 @@ use std::rc::Rc;
use std::time::{Duration, Instant};
use smithay::backend::input::{
- Axis, ButtonState as BackendButtonState, Event as InputEventTrait, InputEvent, PointerAxisEvent,
- PointerButtonEvent, PointerMotionEvent,
+ Axis, ButtonState as BackendButtonState, Event as InputEventTrait, GestureBeginEvent as BackendGestureBeginEvent,
+ GestureEndEvent as BackendGestureEndEvent, GesturePinchUpdateEvent as BackendGesturePinchUpdateEvent, InputEvent,
+ PointerAxisEvent, PointerButtonEvent, PointerMotionEvent,
};
use smithay::backend::libinput::{LibinputInputBackend, LibinputSessionInterface};
use smithay::backend::renderer::damage::OutputDamageTracker;
@@ -47,7 +48,10 @@ use smithay::backend::udev::{self, UdevBackend, UdevEvent};
use smithay::desktop::{layer_map_for_output, PopupManager, Space};
use smithay::backend::input::AxisSource;
use smithay::wayland::shell::wlr_layer::Layer;
-use smithay::input::pointer::AxisFrame;
+use smithay::input::pointer::{
+ AxisFrame, GestureHoldBeginEvent, GestureHoldEndEvent, GesturePinchBeginEvent, GesturePinchEndEvent,
+ GesturePinchUpdateEvent,
+};
use smithay::input::SeatState;
use smithay::output::{Mode as OutputMode, Output, PhysicalProperties, Subpixel};
use smithay::reexports::calloop::generic::{FdWrapper, Generic};
@@ -63,7 +67,7 @@ use smithay::reexports::pixman::{FormatCode, Image};
use smithay::reexports::rustix;
use smithay::reexports::wayland_server::backend::GlobalId;
use smithay::reexports::wayland_server::{Client, Display, DisplayHandle, ListeningSocket};
-use smithay::utils::{Logical, Physical, Point, Rectangle, Scale, Size, Transform};
+use smithay::utils::{Logical, Physical, Point, Rectangle, Scale, Size, Transform, SERIAL_COUNTER};
use smithay::wayland::compositor::CompositorState;
use smithay::wayland::dmabuf::DmabufState;
use smithay::wayland::selection::data_device::DataDeviceState;
@@ -79,7 +83,10 @@ use srdwm_platform::{Platform, PlatformError, PlatformKind, Result as PlatformRe
use crate::decoration;
use crate::err;
-use crate::input::{handle_keyboard_key_event, handle_pointer_button, handle_pointer_position};
+use crate::input::{
+ handle_gesture_swipe_begin, handle_gesture_swipe_end, handle_gesture_swipe_update, handle_keyboard_key_event,
+ handle_pointer_button, handle_pointer_position,
+};
use crate::state::{ClientState, CompState};
/// A DRM device node, opened through the session (not a raw `File::open`)
@@ -158,6 +165,14 @@ pub(crate) struct UdevState {
/// Pointer position in the *global* space, so it can cross between
/// monitors; clamped to the union of all head rectangles.
pub(crate) pointer_pos: Point<f64, Logical>,
+ /// A clone of the same `LibSeatSession` `platform.rs` opened the DRM
+ /// device with (`LibSeatSession` is cheaply `Clone` - see its own
+ /// derive - all clones share the same underlying seat connection).
+ /// Kept here, reachable from `input.rs`'s keyboard handler, purely so
+ /// `Ctrl+Alt+F<n>` can call `change_vt` on it - nothing else in this
+ /// backend needed the session handle after startup, so it was never
+ /// retained anywhere before this.
+ pub(crate) session: LibSeatSession,
}
impl UdevState {
@@ -226,6 +241,7 @@ impl UdevHead {
}
}
+mod capture;
mod drm;
mod outputs;
mod platform;
diff --git a/crates/wayland/src/udev/platform.rs b/crates/wayland/src/udev/platform.rs
index f562da2..8676a5a 100644
--- a/crates/wayland/src/udev/platform.rs
+++ b/crates/wayland/src/udev/platform.rs
@@ -113,6 +113,7 @@ impl UdevPlatform {
heads,
active: true,
pointer_pos: (width as f64 / 2.0, height as f64 / 2.0).into(),
+ session: session.clone(),
};
let state = CompState {
@@ -141,6 +142,7 @@ impl UdevPlatform {
),
_screencopy_state: crate::screencopy::ScreencopyState::new::<CompState>(&display_handle),
screencopy_pending: Vec::new(),
+ _appmenu_state: crate::appmenu::AppmenuManagerState::new::<CompState>(&display_handle),
_foreign_toplevel_state: crate::foreign_toplevel::ForeignToplevelState::new::<CompState>(&display_handle),
foreign_toplevel_managers: Vec::new(),
foreign_toplevel_handles: HashMap::new(),
@@ -170,15 +172,20 @@ impl UdevPlatform {
cursor_status: smithay::input::pointer::CursorImageStatus::default_named(),
cursor_buffers: crate::cursor::make_buffers(),
last_titlebar_click: None,
+ gesture_swipe: None,
context_menu: None,
context_menu_buffer: None,
+ snap_flyout: None,
+ snap_flyout_buffer: None,
wm: wm.clone(),
surface_to_id: HashMap::new(),
id_to_window: HashMap::new(),
dead_layer_surfaces: HashSet::new(),
+ hidden_layer_surfaces: HashMap::new(),
decorations: HashMap::new(),
border_top_decorations: HashMap::new(),
border_bottom_decorations: HashMap::new(),
+ decoration_signatures: HashMap::new(),
shadow_buffers: HashMap::new(),
rounded_corners_program: None,
content_epoch: HashMap::new(),
@@ -196,6 +203,7 @@ impl UdevPlatform {
xwayland_windows: HashMap::new(),
xwayland_pending: Vec::new(),
ewmh: None,
+ appmenu_registrar: None,
};
let listener = ListeningSocket::bind_auto("wayland", 0..32).map_err(err)?;
@@ -262,9 +270,92 @@ impl Platform for UdevPlatform {
self.state.tick_repeat();
self.display.dispatch_clients(&mut self.state).map_err(err)?;
self.display.flush_clients().map_err(err)?;
+ self.state.apply_registrar_events();
if let Some(ipc) = self.ipc.as_mut() {
if ipc.poll(&self.state.wm) {
self.pending.borrow_mut().push(CoreEvent::WorkspaceChanged);
+ // `ipc.rs`'s `handle_request` (`"focus"`, `"toggle
+ // visibility"`, ...) only ever touches core's `WindowManager`
+ // - it has no handle to `state.space`, which is what
+ // actually renders on top *and* what `space.element_under`
+ // hit-tests against (see `input::focus_window`'s own doc
+ // comment, which fixed every *other* focus path this same
+ // way). Left alone, a dock/AGS "focus" click over IPC moved
+ // core's idea of focus while the window kept rendering, and
+ // hit-testing, underneath whatever was already topmost --
+ // reproduced live: `srd dispatch focus` on a covered Firefox
+ // window raised it in the taskbar/keyboard sense but a
+ // click at its own visible location still landed on the
+ // window still actually on top. Re-syncing here rather than
+ // in `ipc.rs` itself since core is platform-agnostic and
+ // cannot see `state.space`; cheap and safe to call
+ // unconditionally on any IPC mutation, not just ones that
+ // are definitely focus changes - raising an already-topmost
+ // element is a no-op reinsertion.
+ let focused = self.state.wm.borrow().focused_id();
+ if let Some(id) = focused {
+ crate::input::focus_window(&mut self.state, id);
+ }
+ }
+ }
+ // Starts srdwm's own lock UI if `srd dispatch lock` queued a
+ // request since the last poll - see `WindowManager::request_lock`'s
+ // own doc comment for why this crosses the core/backend boundary
+ // as a drained request rather than a direct call. A no-op if
+ // already locked (native or external), same guard `begin_native_
+ // lock` applies itself.
+ if self.state.wm.borrow_mut().drain_lock_request() {
+ self.state.begin_native_lock();
+ }
+ // Same drained-request pattern as the lock check just above, for
+ // `srd capture workspace` - see `WindowManager::request_capture_
+ // workspace`'s own doc comment for why this needs the backend at
+ // all rather than being answerable from core state.
+ let capture_requests = self.state.wm.borrow_mut().drain_capture_requests();
+ if !capture_requests.is_empty() {
+ self.state.service_capture_requests(capture_requests);
+ }
+ // Checks whether a background PAM authentication spawned by a
+ // native lock's own `Return` handling finished since the last
+ // poll - see `native_lock.rs`'s module doc comment for why this
+ // runs on a background thread rather than blocking here.
+ self.state.poll_native_lock_auth();
+ // Applies any `srd set_output_position` IPC requests queued since
+ // the last poll - see `WindowManager::request_output_position`'s
+ // own doc comment for why this indirection exists at all (core has
+ // no real output handle to move itself). `id` is this head's index
+ // into `udev.heads` *as of the platform's last `monitors()` query*
+ // (see that function's own construction of `Monitor::new(i as u32,
+ // ...)`) - stale if a hotplug reordered heads in between, same
+ // trade-off `wlr-output-management-v1`'s own `apply_or_test`
+ // guards against with a serial check. Not guarded the same way
+ // here: this is a first pass at the primitive a display-settings
+ // panel needs to build real monitor mirroring on top of, not yet
+ // hardened against a hotplug racing an in-flight request - worth
+ // adding if that turns out to matter in practice.
+ let output_requests = self.state.wm.borrow_mut().drain_output_position_requests();
+ if !output_requests.is_empty() {
+ let mut any_applied = false;
+ for (id, x, y) in output_requests {
+ let Some(output) = self.state.udev.as_ref().and_then(|u| u.heads.get(id as usize)).map(|h| h.output.clone()) else {
+ log::warn!("udev: set_output_position: no head at index {id}");
+ continue;
+ };
+ crate::output_management::apply_output_position(&mut self.state, &output, (x, y).into());
+ any_applied = true;
+ }
+ if any_applied {
+ crate::output_management::broadcast_dirty_outputs(&mut self.state);
+ // Core's own `Monitor` list is a passive mirror of whatever
+ // the backend last reported (see `monitors()` above) --
+ // without re-triggering a query, `Window.geometry`/
+ // placement would keep using the pre-move rect until some
+ // unrelated event happened to refresh it. `MonitorAdded`'s
+ // payload is discarded unread on this path (`main.rs`
+ // re-queries the full list rather than trusting it), same
+ // as every other "just go recompute" use of this event
+ // elsewhere in this codebase.
+ self.pending.borrow_mut().push(CoreEvent::MonitorAdded(srdwm_core::Monitor::new(0, "", srdwm_core::Rect::new(0, 0, 0, 0))));
}
}
self.state.render_udev_frame();
@@ -313,6 +404,7 @@ impl Platform for UdevPlatform {
// the resulting geometry back over IPC, not just from
// reading this code.
m.full_geometry = srdwm_core::Rect::new(head.location.x, head.location.y, head.size.0 as u32, head.size.1 as u32);
+ m.maximize_geometry = crate::input::maximize_geometry_for(&head.output, m.full_geometry);
m.primary = i == 0;
m
})
@@ -396,4 +488,23 @@ impl Platform for UdevPlatform {
fn ungrab_keyboard(&mut self) -> PlatformResult<()> {
Ok(())
}
+
+ fn keyboard_layout(&mut self) -> PlatformResult<String> {
+ let Some(keyboard) = self.state.seat.get_keyboard() else { return Ok(String::new()) };
+ Ok(keyboard.with_xkb_state(&mut self.state, |ctx| {
+ let xkb = ctx.xkb().lock().unwrap();
+ let layout = xkb.active_layout();
+ xkb.layout_name(layout).to_string()
+ }))
+ }
+
+ fn cycle_keyboard_layout(&mut self) -> PlatformResult<String> {
+ let Some(keyboard) = self.state.seat.get_keyboard() else { return Ok(String::new()) };
+ Ok(keyboard.with_xkb_state(&mut self.state, |mut ctx| {
+ ctx.cycle_next_layout();
+ let xkb = ctx.xkb().lock().unwrap();
+ let layout = xkb.active_layout();
+ xkb.layout_name(layout).to_string()
+ }))
+ }
}
diff --git a/crates/wayland/src/udev/render.rs b/crates/wayland/src/udev/render.rs
index caf0e0e..9642603 100644
--- a/crates/wayland/src/udev/render.rs
+++ b/crates/wayland/src/udev/render.rs
@@ -17,6 +17,15 @@ impl CompState {
// Same reason: the cursor needs the renderer that borrow owns.
let cursor_status = self.cursor_status.clone();
let cursor_buffers = self.cursor_buffers.clone();
+ // Same reason again: a native lock's capture step (below) needs
+ // this, and `self.wm` can't be borrowed once `self.udev` is.
+ let lock_blur_radius = self.wm.borrow().lock.blur_radius;
+ // Captured-and-blurred backgrounds collected during the per-head
+ // loop below, applied via `self.capture_output` only after it
+ // ends - `self.udev`'s mutable borrow is held for the whole loop
+ // body, and that method needs the whole of `self`, not just the
+ // one field the loop already has.
+ let mut new_captures: Vec<(String, smithay::backend::renderer::element::memory::MemoryRenderBuffer)> = Vec::new();
// Border geometry is in global space, independent of which head
// renders it, so it's gathered once here rather than per head.
@@ -77,6 +86,12 @@ impl CompState {
let mut presented: Vec<(Output, Vec<Rectangle<i32, Physical>>)> = Vec::new();
for (index, output) in ready {
let lock_surface = self.lock_surface_for(&output).cloned();
+ // Extracted before the `self.udev` borrow below starts - see
+ // `native_lock::native_lock_render_elements`'s own doc comment
+ // for why (cheap `MemoryRenderBuffer` clones, not a pixel copy).
+ let native_bg = self.native_lock_background(&output.name()).cloned();
+ let native_ui = self.native_lock_ui().map(|(buf, size)| (buf.clone(), size));
+ let native_needs_capture = self.native_lock_needs_capture(&output.name());
// Content/decoration elements are built per head: both need the
// renderer, and geometry is translated into head-local space.
@@ -112,6 +127,15 @@ impl CompState {
Err(e) => log::warn!("udev: failed to import context menu buffer: {e}"),
}
}
+ // The Snap-Layouts flyout, if open - same "topmost but
+ // never hides the cursor" placement as the context menu.
+ if let (Some(flyout), Some(buffer)) = (self.snap_flyout.as_ref(), self.snap_flyout_buffer.as_ref()) {
+ let pos = ((flyout.pos.0 - origin.x) as f64, (flyout.pos.1 - origin.y) as f64);
+ match MemoryRenderBufferRenderElement::from_buffer(&mut udev.renderer, pos, buffer, None, None, None, Kind::Unspecified) {
+ Ok(elem) => custom_elements.push(crate::elements::OverlayElement::Memory(elem)),
+ Err(e) => log::warn!("udev: failed to import snap flyout buffer: {e}"),
+ }
+ }
// Popups next: always above every window's own content,
// matching this codebase's long-standing behavior from
// before content moved into this same `custom_elements`
@@ -316,7 +340,7 @@ impl CompState {
// bitmap.
let corners = if w.decorated { crate::rounded_corners::RoundedCorners::BOTTOM_ONLY } else { crate::rounded_corners::RoundedCorners::ALL };
if let Some(buffer) =
- crate::elements::rounded_content_buffer(&mut self.rounded_content_buffers, epoch, id, &surface, decoration::CORNER_RADIUS as f32, corners)
+ crate::elements::rounded_content_buffer(&mut self.rounded_content_buffers, epoch, id, &surface, w.corner_radius as f32, corners)
{
match MemoryRenderBufferRenderElement::from_buffer(&mut udev.renderer, (pos.0 as f64, pos.1 as f64), buffer, Some(w.opacity), None, None, Kind::Unspecified)
{
@@ -327,7 +351,9 @@ impl CompState {
}
match rounded_elem {
Some(elem) => custom_elements.push(crate::elements::OverlayElement::Memory(elem)),
- None => custom_elements.extend(crate::elements::surface_content_elements(&mut udev.renderer, &surface, pos, w.opacity)),
+ None => {
+ custom_elements.extend(crate::elements::surface_content_elements(&mut udev.renderer, &surface, pos, w.opacity));
+ }
}
}
}
@@ -343,11 +369,23 @@ impl CompState {
|layer| matches!(layer, Layer::Background | Layer::Bottom),
));
}
- let lock_elements = if locked {
+ // Three genuinely different element types (external `LockSurface`
+ // content, srdwm's own memory-backed background+UI, or the
+ // normal desktop's `custom_elements`), so each is built and
+ // passed to its own `render_output` call below rather than
+ // forced into one shared, unified element list.
+ let is_native = self.lock.native.is_some();
+ let lock_elements = if locked && !is_native {
crate::lock::lock_render_elements(lock_surface.as_ref(), &mut udev.renderer)
} else {
Vec::new()
};
+ let native_elements = if locked && is_native {
+ let size = udev.heads[index].size;
+ crate::native_lock::native_lock_render_elements(native_bg.as_ref(), native_ui.as_ref().map(|(b, s)| (b, *s)), size, &mut udev.renderer)
+ } else {
+ Vec::new()
+ };
let head = &mut udev.heads[index];
let mut framebuffer = match udev.renderer.bind(&mut head.buffers[back].image) {
@@ -358,9 +396,18 @@ impl CompState {
}
};
- // Locked heads draw the lock surface over opaque black and
- // nothing else; unlocked heads draw the normal scene.
- let result = if locked {
+ // Locked heads draw either srdwm's own native lock UI (over
+ // opaque black - the background element covers the visible
+ // area, but the clear colour is still what shows through if a
+ // capture failed or hasn't happened for this output yet) or an
+ // external locker's surface the same way, and nothing else;
+ // unlocked heads draw the normal scene.
+ let result = if locked && is_native {
+ head.damage_tracker
+ .render_output(&mut udev.renderer, &mut framebuffer, 0, &native_elements, [0.0, 0.0, 0.0, 1.0])
+ .map(|r| (r.damage.is_some(), Vec::new()))
+ .map_err(|e| e.to_string())
+ } else if locked {
head.damage_tracker
.render_output(&mut udev.renderer, &mut framebuffer, 0, &lock_elements, [0.0, 0.0, 0.0, 1.0])
.map(|r| (r.damage.is_some(), Vec::new()))
@@ -401,6 +448,22 @@ impl CompState {
let (mine, rest): (Vec<_>, Vec<_>) = captures.into_iter().partition(|c| c.output == output);
captures = rest;
crate::screencopy::service_pending(mine, &mut udev.renderer, &framebuffer);
+
+ // A native lock is waiting on this output's background --
+ // this same freshly-rendered framebuffer (the ordinary
+ // desktop scene, not a lock scene: `locked` is still
+ // `false` here because `begin_native_lock` deliberately
+ // doesn't flip it until every output has one, see that
+ // function's own doc comment) is exactly "what's on
+ // screen right now" for this output.
+ if native_needs_capture {
+ let name = output.name();
+ let size = head.size;
+ match crate::native_lock::capture_and_blur(&mut udev.renderer, &framebuffer, size, lock_blur_radius) {
+ Ok(blurred) => new_captures.push((name, blurred)),
+ Err(e) => log::warn!("native lock: capture failed for output {name}: {e}"),
+ }
+ }
}
drop(framebuffer);
@@ -441,6 +504,15 @@ impl CompState {
}
}
+ // Applies every background captured during the loop above, now
+ // that `self.udev`'s borrow has ended and `self` (specifically
+ // `self.lock`) can be borrowed as a whole again - see
+ // `capture_output`'s own doc comment for what happens once every
+ // output has one (the lock actually engages).
+ for (name, blurred) in new_captures {
+ self.capture_output(&name, blurred);
+ }
+
// Frame callbacks + lock confirmation, once the `udev` borrow is done.
for (output, damage_rects) in presented {
if locked {
diff --git a/crates/wayland/src/udev/session.rs b/crates/wayland/src/udev/session.rs
index fa40b0e..4dd6db0 100644
--- a/crates/wayland/src/udev/session.rs
+++ b/crates/wayland/src/udev/session.rs
@@ -214,6 +214,41 @@ fn handle_libinput_event(state: &mut CompState, event: InputEvent<LibinputInputB
pointer.axis(state, frame);
pointer.frame(state);
}
+ // 3+-finger swipe - claimed entirely for workspace switching, never
+ // reaches a client. See `handle_gesture_swipe_end`'s doc comment.
+ InputEvent::GestureSwipeBegin { event } => handle_gesture_swipe_begin(state, &event),
+ InputEvent::GestureSwipeUpdate { event } => handle_gesture_swipe_update(state, &event),
+ InputEvent::GestureSwipeEnd { event } => handle_gesture_swipe_end(state, &event),
+ // Pinch/hold: no WM-level meaning, forwarded to the focused client
+ // as-is (`wp_pointer_gestures`) - pinch-to-zoom in an image viewer
+ // or PDF reader, the one real use either has. Same reasoning as the
+ // `PointerAxis` forwarding above: nothing here should be silently
+ // dropped just because this WM has no use for it itself.
+ InputEvent::GesturePinchBegin { event } => {
+ let Some(pointer) = state.seat.get_pointer() else { return };
+ let fingers = event.fingers();
+ pointer.gesture_pinch_begin(state, &GesturePinchBeginEvent { serial: SERIAL_COUNTER.next_serial(), time: event.time_msec(), fingers });
+ }
+ InputEvent::GesturePinchUpdate { event } => {
+ let Some(pointer) = state.seat.get_pointer() else { return };
+ let (delta, scale, rotation) = (event.delta(), event.scale(), event.rotation());
+ pointer.gesture_pinch_update(state, &GesturePinchUpdateEvent { time: event.time_msec(), delta, scale, rotation });
+ }
+ InputEvent::GesturePinchEnd { event } => {
+ let Some(pointer) = state.seat.get_pointer() else { return };
+ let cancelled = event.cancelled();
+ pointer.gesture_pinch_end(state, &GesturePinchEndEvent { serial: SERIAL_COUNTER.next_serial(), time: event.time_msec(), cancelled });
+ }
+ InputEvent::GestureHoldBegin { event } => {
+ let Some(pointer) = state.seat.get_pointer() else { return };
+ let fingers = event.fingers();
+ pointer.gesture_hold_begin(state, &GestureHoldBeginEvent { serial: SERIAL_COUNTER.next_serial(), time: event.time_msec(), fingers });
+ }
+ InputEvent::GestureHoldEnd { event } => {
+ let Some(pointer) = state.seat.get_pointer() else { return };
+ let cancelled = event.cancelled();
+ pointer.gesture_hold_end(state, &GestureHoldEndEvent { serial: SERIAL_COUNTER.next_serial(), time: event.time_msec(), cancelled });
+ }
_ => {}
}
}