diff options
| author | srdusr <[email protected]> | 2024-12-24 20:44:00 +0200 |
|---|---|---|
| committer | srdusr <[email protected]> | 2024-12-24 20:44:00 +0200 |
| commit | 3d73ed0f057a555ded67ad58dbe02b36cd4d2d1f (patch) | |
| tree | 71e9bab462b9073444183e2a969ba512cb94559f /crates/wayland/src/udev | |
| parent | 331a1a1d3a0d8dfd4c285b755c2e13785c95ee2a (diff) | |
| download | srdwm-3d73ed0f057a555ded67ad58dbe02b36cd4d2d1f.tar.gz srdwm-3d73ed0f057a555ded67ad58dbe02b36cd4d2d1f.zip | |
Accumulate wayland-crate additions: capture rendering, focus/Space sync, VT resume, plumbing
Bundles the remaining wayland-crate changes built up here,
touching both backends (udev and winit) and the shared input/rendering
code:
- udev/capture.rs: off-screen Pixman render of an arbitrary (not
necessarily on-screen) workspace's window content to a PPM file --
what crates/core's capture-request queue drives, for a workspace
switcher's thumbnail previews. wlr-screencopy structurally can't do
this (it can only see what an output is presenting), which is why
this exists as a separate render path rather than reusing it.
- input::focus_window now also raises the window in smithay's own
Space, not just core's stacking order - Space is what actually
renders on top and what pointer hit-testing reads, so any focus path
that skipped this (an IPC "focus" dispatch, concretely) left a
window genuinely focused while still rendering, and receiving
clicks, underneath whatever was already topmost. Both backends'
poll loops now re-sync this after any IPC mutation.
- udev/session.rs's VT-switch resume fix (drains a stale pending page
flip before reasserting CRTCs) already has its own earlier, cleanly
isolated commit - not duplicated here.
- Assorted decoration/cursor/rounded-corners/output-management/
screencopy/XWayland changes and their cross-backend wiring.
Coarser than the repo's usual one-purpose-per-commit convention,
deliberately - see the core-crate sweep commit's own message for why.
Diffstat (limited to 'crates/wayland/src/udev')
| -rw-r--r-- | crates/wayland/src/udev/capture.rs | 129 | ||||
| -rw-r--r-- | crates/wayland/src/udev/mod.rs | 26 | ||||
| -rw-r--r-- | crates/wayland/src/udev/platform.rs | 111 | ||||
| -rw-r--r-- | crates/wayland/src/udev/render.rs | 84 | ||||
| -rw-r--r-- | crates/wayland/src/udev/session.rs | 35 |
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 }); + } _ => {} } } |