Files
netris-nestri/apps/nescope/src/handlers.rs
T
0811f57f1a feat: media bitrate control, HDR (#346)
Fixes: #335 

Still a work-in-progress.

---------

Co-authored-by: DatCaptainHorse <DatCaptainHorse@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Wanjohi <elviswanjohi47@gmail.com>
2026-09-25 12:13:34 +03:00

610 lines
22 KiB
Rust

//! Smithay protocol handler implementations for nescope.
//!
//! - `CompositorHandler` on `NescopeState`
//! - `XdgShellHandler` on `NescopeState`
//! - `SeatHandler` on `NescopeState`
//! - `XwmHandler` on `CalloopData` (calloop dispatch type) + stub on `NescopeState`
//! - All delegate macros
use smithay::backend::allocator::dmabuf::Dmabuf;
use smithay::desktop::Window;
use smithay::input::pointer::{CursorImageStatus, PointerHandle};
use smithay::input::{Seat, SeatHandler, SeatState};
use smithay::output::Output;
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
use smithay::reexports::wayland_server::protocol::wl_buffer;
use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
use smithay::reexports::wayland_server::protocol::wl_seat::WlSeat;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::utils::{Logical, Point, Rectangle, Serial};
use smithay::wayland::buffer::BufferHandler;
use smithay::wayland::compositor::{
BufferAssignment, CompositorClientState, CompositorHandler, CompositorState, SurfaceAttributes,
is_sync_subsurface, with_states,
};
use smithay::wayland::dmabuf::{DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier};
use smithay::wayland::output::OutputHandler;
use smithay::wayland::pointer_constraints::{PointerConstraintsHandler, with_pointer_constraint};
use smithay::wayland::seat::WaylandFocus;
use smithay::wayland::selection::SelectionHandler;
use smithay::wayland::selection::data_device::{
ClientDndGrabHandler, DataDeviceHandler, DataDeviceState, ServerDndGrabHandler,
};
use smithay::wayland::shell::xdg::{
PopupSurface, PositionerState, ToplevelSurface, XdgShellHandler, XdgShellState,
};
use smithay::wayland::shm::{ShmHandler, ShmState, with_buffer_contents};
use smithay::wayland::xwayland_shell::{XWaylandShellHandler, XWaylandShellState};
use smithay::xwayland::xwm::{Reorder, ResizeEdge, WmWindowProperty, XwmId};
use smithay::xwayland::{X11Surface, X11Wm, XWaylandClientData, XwmHandler};
use crate::focus::KeyboardFocusTarget;
use crate::state::{CalloopData, ClientState, NescopeState};
// ===========================================================================
// BufferHandler / ShmHandler
// ===========================================================================
impl BufferHandler for NescopeState {
fn buffer_destroyed(&mut self, _buffer: &wl_buffer::WlBuffer) {}
}
impl ShmHandler for NescopeState {
fn shm_state(&self) -> &ShmState {
&self.shm_state
}
}
// ===========================================================================
// CompositorHandler
// ===========================================================================
impl CompositorHandler for NescopeState {
fn compositor_state(&mut self) -> &mut CompositorState {
&mut self.compositor_state
}
fn client_compositor_state<'a>(
&self,
client: &'a smithay::reexports::wayland_server::Client,
) -> &'a CompositorClientState {
if let Some(state) = client.get_data::<ClientState>() {
return &state.compositor_state;
}
if let Some(state) = client.get_data::<XWaylandClientData>() {
return &state.compositor_state;
}
panic!("Client has neither ClientState nor XWaylandClientData");
}
fn commit(&mut self, surface: &WlSurface) {
self.hdr.commit(surface);
if is_sync_subsurface(surface) {
return;
}
// Hold a reference to the committed buffer — delays wl_buffer.release
// until the next frame tick, starving the swapchain of images in FIFO mode.
smithay::wayland::compositor::with_states(surface, |states| {
let mut cached = states
.cached_state
.get::<smithay::wayland::compositor::SurfaceAttributes>();
let attrs = cached.current();
if let Some(smithay::wayland::compositor::BufferAssignment::NewBuffer(ref wl_buf)) =
attrs.buffer
{
self.held_buffer = Some(wl_buf.clone());
}
});
// Notify the window of the commit so it can refresh its cached state.
if let Some(window) = self
.space
.elements()
.find(|w| {
w.toplevel()
.map(|t| t.wl_surface() == surface)
.unwrap_or(false)
})
.cloned()
{
window.on_commit();
// A game frame, which is what the stats claim to report. Counted
// here rather than on the frame-callback tick: that tick fires
// whether anything was drawn or not, so counting it reported the
// compositor's own cadence back as the game's rate.
self.game_frame_count += 1;
}
}
fn destroyed(&mut self, surface: &WlSurface) {
self.hdr.surface_destroyed(surface);
}
}
// ===========================================================================
// DmabufHandler — accept everything; nescope never imports the buffers itself
// ===========================================================================
impl DmabufHandler for NescopeState {
fn dmabuf_state(&mut self) -> &mut DmabufState {
&mut self.dmabuf_state
}
fn dmabuf_imported(
&mut self,
_global: &DmabufGlobal,
_dmabuf: Dmabuf,
notifier: ImportNotifier,
) {
// Accept unconditionally — the nescapture layer reads buffers
// directly from the game's Vulkan queue; nescope doesn't need to.
let _ = notifier.successful::<NescopeState>();
}
}
// ===========================================================================
// XDG shell
// ===========================================================================
impl XdgShellHandler for NescopeState {
fn xdg_shell_state(&mut self) -> &mut XdgShellState {
&mut self.xdg_shell_state
}
fn new_toplevel(&mut self, surface: ToplevelSurface) {
// Tell the toplevel to fill the entire virtual output.
surface.with_pending_state(|state| {
state.size = Some((self.width as i32, self.height as i32).into());
});
surface.send_configure();
let window = Window::new_wayland_window(surface);
self.space.map_element(window.clone(), (0, 0), false);
self.set_keyboard_focus_to_window(&window);
tracing::debug!("New XDG toplevel");
}
fn toplevel_destroyed(&mut self, surface: ToplevelSurface) {
// Remove the dead window from the space.
let elem = self
.space
.elements()
.find(|w| {
w.toplevel()
.map(|t| t.wl_surface() == surface.wl_surface())
.unwrap_or(false)
})
.cloned();
if let Some(elem) = elem {
self.space.unmap_elem(&elem);
tracing::debug!("Unmapped destroyed XDG toplevel");
}
// Re-focus the next window (if any).
self.determine_and_apply_focus();
}
// Granted, not merely acknowledged. The default answers with a configure
// that lacks the fullscreen state, which a client reads as a refusal: Wine
// then asks again on every window update and never treats its window as
// fullscreen, so a game switching to exclusive fullscreen -- Control does
// this on leaving its title screen -- stalls in the transition and stays
// where it was. Wine also scales an emulated display mode up to the output
// only for a fullscreen window.
fn fullscreen_request(&mut self, surface: ToplevelSurface, _output: Option<WlOutput>) {
surface.with_pending_state(|state| {
state.states.set(xdg_toplevel::State::Fullscreen);
state.size = Some((self.width as i32, self.height as i32).into());
});
surface.send_configure();
}
// Still the size of the output: there is nowhere else for a window to be.
fn unfullscreen_request(&mut self, surface: ToplevelSurface) {
surface.with_pending_state(|state| {
state.states.unset(xdg_toplevel::State::Fullscreen);
});
surface.send_configure();
}
fn new_popup(&mut self, _: PopupSurface, _: PositionerState) {}
fn grab(&mut self, _: PopupSurface, _: WlSeat, _: Serial) {}
fn reposition_request(&mut self, _: PopupSurface, _: PositionerState, _: u32) {}
}
// ===========================================================================
// SeatHandler
// ===========================================================================
impl SeatHandler for NescopeState {
type KeyboardFocus = KeyboardFocusTarget;
type PointerFocus = KeyboardFocusTarget;
type TouchFocus = WlSurface;
fn seat_state(&mut self) -> &mut SeatState<Self> {
&mut self.seat_state
}
fn focus_changed(&mut self, _seat: &Seat<Self>, focused: Option<&KeyboardFocusTarget>) {
let kind = focused
.map(|f| match f {
KeyboardFocusTarget::Window(_) => "wayland window",
KeyboardFocusTarget::ProxiedX11 { .. } => "proxied x11",
})
.unwrap_or("none");
tracing::debug!("keyboard focus changed to {kind}");
}
fn cursor_image(&mut self, _seat: &Seat<Self>, image: CursorImageStatus) {
let kind = match &image {
CursorImageStatus::Hidden => "hidden",
CursorImageStatus::Named(n) => {
tracing::debug!("cursor: named({n})");
"named"
}
CursorImageStatus::Surface(_) => {
tracing::debug!("cursor: custom surface");
"surface"
}
};
tracing::trace!("cursor_image set to {kind}");
// Try to capture custom cursor pixels from SHM buffer
let captured = if let CursorImageStatus::Surface(ref surface) = image {
let buffer_opt: Option<wl_buffer::WlBuffer> = with_states(surface, |data| {
let mut attrs = data.cached_state.get::<SurfaceAttributes>();
match attrs.current().buffer {
Some(BufferAssignment::NewBuffer(ref buf)) => Some(buf.clone()),
_ => None,
}
});
buffer_opt.and_then(|buffer| {
match with_buffer_contents(&buffer, |ptr, _len, data| {
let offset = data.offset as isize;
let src = unsafe { ptr.offset(offset) };
let pixel_bytes = (data.width * data.height * 4) as usize;
let stride = data.stride as usize;
let is_xrgb = data.format
== smithay::reexports::wayland_server::protocol::wl_shm::Format::Xrgb8888;
let mut rgba = vec![0u8; pixel_bytes];
for row in 0..data.height as usize {
let src_start = row * stride;
let dst_row = (data.height as usize - 1 - row) * data.width as usize * 4;
unsafe {
std::ptr::copy_nonoverlapping(
src.add(src_start),
rgba.as_mut_ptr().add(dst_row),
data.width as usize * 4,
);
}
}
if is_xrgb {
for px in rgba.chunks_exact_mut(4) {
px[3] = 255;
}
}
let preview = if rgba.len() >= 16 {
&rgba[..16]
} else {
&rgba[..]
};
tracing::debug!(
"cursor: captured {}x{} xrgb={} preview={:02x?}",
data.width,
data.height,
is_xrgb,
preview,
);
nesprotocol::input::CursorImageData {
x: 0.0,
y: 0.0,
width: data.width as u16,
height: data.height as u16,
hotspot_x: 0,
hotspot_y: 0,
rgba,
}
}) {
Ok(d) => Some(d),
Err(_) => {
tracing::debug!("cursor: surface buffer is not SHM, using box fallback");
None
}
}
})
} else {
None
};
self.cursor_image_data = captured;
self.cursor_image_sent = false; // allow re-send for new cursor surface
if self.cursor_image_data.is_some() {
tracing::debug!(
"cursor: captured custom image {}x{} (BGRA)",
self.cursor_image_data.as_ref().unwrap().width,
self.cursor_image_data.as_ref().unwrap().height,
);
}
self.cursor_status = image;
}
fn led_state_changed(&mut self, _: &Seat<Self>, _: smithay::input::keyboard::LedState) {}
}
// ===========================================================================
// Selection / data device
// ===========================================================================
impl SelectionHandler for NescopeState {
type SelectionUserData = ();
}
impl DataDeviceHandler for NescopeState {
fn data_device_state(&self) -> &DataDeviceState {
&self.data_device_state
}
}
impl ClientDndGrabHandler for NescopeState {}
impl ServerDndGrabHandler for NescopeState {}
// ===========================================================================
// Output
// ===========================================================================
impl OutputHandler for NescopeState {
fn output_bound(&mut self, _output: Output, _wl_output: WlOutput) {}
}
// ===========================================================================
// Pointer constraints
// ===========================================================================
impl PointerConstraintsHandler for NescopeState {
fn new_constraint(&mut self, surface: &WlSurface, pointer: &PointerHandle<Self>) {
if let Some(focus) = pointer.current_focus() {
if focus.wl_surface().as_deref() == Some(surface) {
with_pointer_constraint(surface, pointer, |c| {
if let Some(c) = c {
c.activate();
}
});
}
}
}
fn cursor_position_hint(
&mut self,
surface: &WlSurface,
pointer: &PointerHandle<Self>,
location: Point<f64, Logical>,
) {
if with_pointer_constraint(surface, pointer, |c| c.is_some_and(|c| c.is_active())) {
use smithay::wayland::seat::WaylandFocus;
let origin = self
.space
.elements()
.find_map(|w| (w.wl_surface().as_deref() == Some(surface)).then(|| w.geometry()))
.unwrap_or_default()
.loc
.to_f64();
pointer.set_location(origin + location);
}
}
}
// ===========================================================================
// XWayland shell
// ===========================================================================
impl XWaylandShellHandler for NescopeState {
fn xwayland_shell_state(&mut self) -> &mut XWaylandShellState {
&mut self.xwayland_shell_state
}
fn surface_associated(&mut self, _xwm: XwmId, _wl_surface: WlSurface, surface: X11Surface) {
tracing::debug!(window_id = surface.window_id(), "X11 surface associated");
self.determine_and_apply_focus();
}
}
impl XWaylandShellHandler for CalloopData {
fn xwayland_shell_state(&mut self) -> &mut XWaylandShellState {
&mut self.state.xwayland_shell_state
}
fn surface_associated(&mut self, xwm: XwmId, wl_surface: WlSurface, surface: X11Surface) {
XWaylandShellHandler::surface_associated(&mut self.state, xwm, wl_surface, surface);
}
}
// ===========================================================================
// XwmHandler — real implementation on CalloopData
// ===========================================================================
impl XwmHandler for CalloopData {
fn xwm_state(&mut self, _xwm: XwmId) -> &mut X11Wm {
self.state.xwm.as_mut().expect("XWM not initialized")
}
fn new_window(&mut self, _xwm: XwmId, window: X11Surface) {
tracing::debug!(
window_id = window.window_id(),
title = ?window.title(),
"New X11 window"
);
}
fn new_override_redirect_window(&mut self, _xwm: XwmId, window: X11Surface) {
tracing::debug!(
window_id = window.window_id(),
"New override-redirect window"
);
}
fn map_window_request(&mut self, _xwm: XwmId, window: X11Surface) {
tracing::debug!(
title = ?window.title(),
class = ?window.class(),
"X11 map_window_request"
);
let geo = Rectangle::new(
(0, 0).into(),
(self.state.width as i32, self.state.height as i32).into(),
);
if let Err(e) = window.configure(geo) {
tracing::warn!("configure failed: {e}");
}
if let Err(e) = window.set_mapped(true) {
tracing::error!("set_mapped failed: {e}");
return;
}
let win = Window::new_x11_window(window);
self.state.space.map_element(win, (0, 0), true);
self.state.determine_and_apply_focus();
}
fn mapped_override_redirect_window(&mut self, _xwm: XwmId, window: X11Surface) {
let location = window.geometry().loc;
let win = Window::new_x11_window(window);
self.state.space.map_element(win, location, false);
}
fn unmapped_window(&mut self, _xwm: XwmId, window: X11Surface) {
let was_focused = Some(window.window_id()) == self.state.focused_x11_window;
let elem = self
.state
.space
.elements()
.find(|e| e.x11_surface().map(|x| x == &window).unwrap_or(false))
.cloned();
if let Some(elem) = elem {
self.state.space.unmap_elem(&elem);
}
if !window.is_override_redirect() {
let _ = window.set_mapped(false);
}
if was_focused {
self.state.focused_x11_window = None;
self.state.determine_and_apply_focus();
}
}
fn destroyed_window(&mut self, _xwm: XwmId, _window: X11Surface) {}
fn configure_request(
&mut self,
_xwm: XwmId,
window: X11Surface,
_x: Option<i32>,
_y: Option<i32>,
w: Option<u32>,
h: Option<u32>,
_reorder: Option<Reorder>,
) {
// Honor size requests within reason but always keep position at (0,0).
let mut geo = window.geometry();
if let Some(w) = w {
geo.size.w = w as i32;
}
if let Some(h) = h {
geo.size.h = h as i32;
}
let _ = window.configure(geo);
}
fn configure_notify(
&mut self,
_xwm: XwmId,
window: X11Surface,
geometry: Rectangle<i32, Logical>,
_above: Option<u32>,
) {
let target_elem = self
.state
.space
.elements()
.find(|e| e.x11_surface().map(|x| x == &window).unwrap_or(false))
.cloned();
if let Some(elem) = target_elem {
self.state.space.map_element(elem, geometry.loc, false);
}
}
fn property_notify(&mut self, _xwm: XwmId, _window: X11Surface, property: WmWindowProperty) {
// Only recalculate focus when the window class changes (steam_app_* detection).
if matches!(property, WmWindowProperty::Class) {
self.state.determine_and_apply_focus();
}
}
fn resize_request(&mut self, _: XwmId, _: X11Surface, _: u32, _: ResizeEdge) {}
fn move_request(&mut self, _: XwmId, _: X11Surface, _: u32) {}
}
// ===========================================================================
// XwmHandler stub on NescopeState
//
// `delegate_xwayland_shell!(NescopeState)` generates Dispatch impls with a
// `NescopeState: XwmHandler` bound. Only `xwm_state()` is ever called on
// this path (surface association). Real WM logic lives in the CalloopData impl.
// ===========================================================================
impl XwmHandler for NescopeState {
fn xwm_state(&mut self, _xwm: XwmId) -> &mut X11Wm {
self.xwm.as_mut().expect("XWM not initialized")
}
fn new_window(&mut self, _: XwmId, _: X11Surface) {}
fn new_override_redirect_window(&mut self, _: XwmId, _: X11Surface) {}
fn map_window_request(&mut self, _: XwmId, _: X11Surface) {}
fn mapped_override_redirect_window(&mut self, _: XwmId, _: X11Surface) {}
fn unmapped_window(&mut self, _: XwmId, _: X11Surface) {}
fn destroyed_window(&mut self, _: XwmId, _: X11Surface) {}
fn configure_request(
&mut self,
_: XwmId,
_: X11Surface,
_: Option<i32>,
_: Option<i32>,
_: Option<u32>,
_: Option<u32>,
_: Option<Reorder>,
) {
}
fn configure_notify(
&mut self,
_: XwmId,
_: X11Surface,
_: Rectangle<i32, Logical>,
_: Option<u32>,
) {
}
fn property_notify(&mut self, _: XwmId, _: X11Surface, _: WmWindowProperty) {}
fn resize_request(&mut self, _: XwmId, _: X11Surface, _: u32, _: ResizeEdge) {}
fn move_request(&mut self, _: XwmId, _: X11Surface, _: u32) {}
}
// ===========================================================================
// Delegate macros
// ===========================================================================
smithay::delegate_compositor!(NescopeState);
smithay::delegate_dmabuf!(NescopeState);
smithay::delegate_shm!(NescopeState);
smithay::delegate_xdg_shell!(NescopeState);
smithay::delegate_seat!(NescopeState);
smithay::delegate_data_device!(NescopeState);
smithay::delegate_output!(NescopeState);
smithay::delegate_relative_pointer!(NescopeState);
smithay::delegate_pointer_constraints!(NescopeState);
smithay::delegate_xwayland_shell!(NescopeState);
smithay::delegate_viewporter!(NescopeState);
smithay::delegate_presentation!(NescopeState);