Add screenshot-window show-pointer=true

This commit is contained in:
Ivan Molodetskikh
2026-01-06 22:02:03 +03:00
parent a496307daf
commit 9f8eadc5bc
5 changed files with 99 additions and 7 deletions
+11
View File
@@ -382,6 +382,17 @@ binds {
} }
``` ```
<sup>Since: next release</sup> You can show the mouse pointer on window screenshots with the `show-pointer=true` property.
The pointer will be included only if the window is currently receiving pointer input (usually this means the pointer is on top of the window).
```kdl
binds {
// The pointer will be visible on the screenshot
// if it's on top of the window.
Alt+Print { screenshot-window show-pointer=true; }
}
```
#### `toggle-keyboard-shortcuts-inhibit` #### `toggle-keyboard-shortcuts-inhibit`
<sup>Since: 25.02</sup> <sup>Since: 25.02</sup>
+6 -1
View File
@@ -132,6 +132,7 @@ pub enum Action {
), ),
ScreenshotWindow( ScreenshotWindow(
#[knuffel(property(name = "write-to-disk"), default = true)] bool, #[knuffel(property(name = "write-to-disk"), default = true)] bool,
#[knuffel(property(name = "show-pointer"), default = false)] bool,
// Path; not settable from knuffel // Path; not settable from knuffel
Option<String>, Option<String>,
), ),
@@ -139,6 +140,7 @@ pub enum Action {
ScreenshotWindowById { ScreenshotWindowById {
id: u64, id: u64,
write_to_disk: bool, write_to_disk: bool,
show_pointer: bool,
path: Option<String>, path: Option<String>,
}, },
ToggleKeyboardShortcutsInhibit, ToggleKeyboardShortcutsInhibit,
@@ -407,15 +409,18 @@ impl From<niri_ipc::Action> for Action {
niri_ipc::Action::ScreenshotWindow { niri_ipc::Action::ScreenshotWindow {
id: None, id: None,
write_to_disk, write_to_disk,
show_pointer,
path, path,
} => Self::ScreenshotWindow(write_to_disk, path), } => Self::ScreenshotWindow(write_to_disk, show_pointer, path),
niri_ipc::Action::ScreenshotWindow { niri_ipc::Action::ScreenshotWindow {
id: Some(id), id: Some(id),
write_to_disk, write_to_disk,
show_pointer,
path, path,
} => Self::ScreenshotWindowById { } => Self::ScreenshotWindowById {
id, id,
write_to_disk, write_to_disk,
show_pointer,
path, path,
}, },
niri_ipc::Action::ToggleKeyboardShortcutsInhibit {} => { niri_ipc::Action::ToggleKeyboardShortcutsInhibit {} => {
+7
View File
@@ -264,6 +264,13 @@ pub enum Action {
#[cfg_attr(feature = "clap", arg(short = 'd', long, action = clap::ArgAction::Set, default_value_t = true))] #[cfg_attr(feature = "clap", arg(short = 'd', long, action = clap::ArgAction::Set, default_value_t = true))]
write_to_disk: bool, write_to_disk: bool,
/// Whether to include the mouse pointer in the screenshot.
///
/// The pointer will be included only if the window is currently receiving pointer input
/// (usually this means the pointer is on top of the window).
#[cfg_attr(feature = "clap", arg(short = 'p', long, action = clap::ArgAction::Set, default_value_t = false))]
show_pointer: bool,
/// Path to save the screenshot to. /// Path to save the screenshot to.
/// ///
/// The path must be absolute, otherwise an error is returned. /// The path must be absolute, otherwise an error is returned.
+4 -1
View File
@@ -743,7 +743,7 @@ impl State {
self.open_screenshot_ui(show_cursor, path); self.open_screenshot_ui(show_cursor, path);
self.niri.cancel_mru(); self.niri.cancel_mru();
} }
Action::ScreenshotWindow(write_to_disk, path) => { Action::ScreenshotWindow(write_to_disk, show_pointer, path) => {
let focus = self.niri.layout.focus_with_output(); let focus = self.niri.layout.focus_with_output();
if let Some((mapped, output)) = focus { if let Some((mapped, output)) = focus {
self.backend.with_primary_renderer(|renderer| { self.backend.with_primary_renderer(|renderer| {
@@ -752,6 +752,7 @@ impl State {
output, output,
mapped, mapped,
write_to_disk, write_to_disk,
show_pointer,
path, path,
) { ) {
warn!("error taking screenshot: {err:?}"); warn!("error taking screenshot: {err:?}");
@@ -762,6 +763,7 @@ impl State {
Action::ScreenshotWindowById { Action::ScreenshotWindowById {
id, id,
write_to_disk, write_to_disk,
show_pointer,
path, path,
} => { } => {
let mut windows = self.niri.layout.windows(); let mut windows = self.niri.layout.windows();
@@ -774,6 +776,7 @@ impl State {
output, output,
mapped, mapped,
write_to_disk, write_to_disk,
show_pointer,
path, path,
) { ) {
warn!("error taking screenshot: {err:?}"); warn!("error taking screenshot: {err:?}");
+71 -5
View File
@@ -139,7 +139,9 @@ use crate::layer::mapped::LayerSurfaceRenderElement;
use crate::layer::MappedLayer; use crate::layer::MappedLayer;
use crate::layout::tile::TileRenderElement; use crate::layout::tile::TileRenderElement;
use crate::layout::workspace::{Workspace, WorkspaceId}; use crate::layout::workspace::{Workspace, WorkspaceId};
use crate::layout::{HitType, Layout, LayoutElement as _, MonitorRenderElement}; use crate::layout::{
HitType, Layout, LayoutElement as _, LayoutElementRenderElement, MonitorRenderElement,
};
use crate::niri_render_elements; use crate::niri_render_elements;
use crate::protocols::ext_workspace::{self, ExtWorkspaceManagerState}; use crate::protocols::ext_workspace::{self, ExtWorkspaceManagerState};
use crate::protocols::foreign_toplevel::{self, ForeignToplevelManagerState}; use crate::protocols::foreign_toplevel::{self, ForeignToplevelManagerState};
@@ -5665,6 +5667,7 @@ impl Niri {
output: &Output, output: &Output,
mapped: &Mapped, mapped: &Mapped,
write_to_disk: bool, write_to_disk: bool,
show_pointer: bool,
path: Option<String>, path: Option<String>,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let _span = tracy_client::span!("Niri::screenshot_window"); let _span = tracy_client::span!("Niri::screenshot_window");
@@ -5676,17 +5679,73 @@ impl Niri {
} else { } else {
mapped.rules().opacity.unwrap_or(1.).clamp(0., 1.) mapped.rules().opacity.unwrap_or(1.).clamp(0., 1.)
}; };
// FIXME: pointer.
let mut elements = Vec::new(); let mut elements: Vec<WindowScreenshotRenderElement<GlesRenderer>> = Vec::new();
// Add pointer if requested and it's over this window.
if show_pointer {
if let Some((w, HitType::Input { win_pos })) = &self.pointer_contents.window {
if w == &mapped.window {
// Grabs can modify the pointer focus, making it different from
// pointer_contents. Notably, gestures like Mod+MMB will remove the pointer
// focus, and ClickGrab will keep pointer focus on the clicked window even
// while it's moving over a different window.
//
// So, double-check that current_focus() (after grabs) also matches the pointer
// contents.
let pointer = self.seat.get_pointer().unwrap();
// The DnD grab is a bit special because it has its own focus (data device)
// while the pointer focus is cleared. That focus is not currently exposed from
// Smithay, and showing DnD icons on window screenshots seems useful, so let's
// just allow it during DnD grabs.
let is_dnd_grab = pointer
.with_grab(|_, grab| State::is_dnd_grab(grab.as_any()))
.unwrap_or(false);
let current_focus_matches = is_dnd_grab
|| pointer
.current_focus()
.map(|focused| self.find_root_shell_surface(&focused))
.is_some_and(|focused| mapped.is_wl_surface(&focused));
if current_focus_matches {
// win_pos is the window buffer position in output-local logical coords.
let win_pos = win_pos.to_physical_precise_round(scale);
// We don't check for pointer visibility because it can only be Visible or
// Hidden, and never Disabled (then it wouldn't have focus). Even when the
// pointer is Hidden, we want to render it, since the user explicitly
// requested show_pointer = true, and otherwise there's no easy way to
// screenshot a window with pointer with hide-when-typing because pressing
// the screenshot bind will hide the pointer.
self.render_pointer(renderer, output, &mut |elem| {
// Pointer elements are at output-local physical coords.
// Relocate by -win_pos to make them window-relative.
let elem = RelocateRenderElement::from_element(
elem,
win_pos.upscale(-1),
Relocate::Relative,
);
elements.push(elem.into());
});
}
}
}
}
let pointer_count = elements.len();
mapped.render( mapped.render(
renderer, renderer,
mapped.window.geometry().loc.to_f64(), mapped.window.geometry().loc.to_f64(),
scale, scale,
alpha, alpha,
RenderTarget::ScreenCapture, RenderTarget::ScreenCapture,
&mut |elem| elements.push(elem), &mut |elem| elements.push(elem.into()),
); );
let geo = encompassing_geo(scale, elements.iter());
// The pointer is not included in encompassing_geo because we don't want it to expand the
// screenshot size.
let geo = encompassing_geo(scale, elements.iter().skip(pointer_count));
let elements = elements.iter().rev().map(|elem| { let elements = elements.iter().rev().map(|elem| {
RelocateRenderElement::from_element(elem, geo.loc.upscale(-1), Relocate::Relative) RelocateRenderElement::from_element(elem, geo.loc.upscale(-1), Relocate::Relative)
}); });
@@ -6557,6 +6616,13 @@ niri_render_elements! {
} }
} }
niri_render_elements! {
WindowScreenshotRenderElement<R> => {
Layout = LayoutElementRenderElement<R>,
Pointer = RelocateRenderElement<PointerRenderElements<R>>,
}
}
niri_render_elements! { niri_render_elements! {
OutputRenderElements<R> => { OutputRenderElements<R> => {
Monitor = MonitorRenderElement<R>, Monitor = MonitorRenderElement<R>,