Fix compositor output routing and pointer hints; format imports
This commit is contained in:
1 parent
88e4f3c135
commit
46509f880a
108 files changed
+3682
-8371
No files matched your search
@@ -1,11 +1,9 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use smithay::{
|
||||
reexports::wayland_server::protocol::wl_surface::WlSurface,
|
||||
utils::SERIAL_COUNTER,
|
||||
wayland::xdg_activation::{
|
||||
XdgActivationHandler, XdgActivationState, XdgActivationToken, XdgActivationTokenData,
|
||||
},
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::utils::SERIAL_COUNTER;
|
||||
use smithay::wayland::xdg_activation::{
|
||||
XdgActivationHandler, XdgActivationState, XdgActivationToken, XdgActivationTokenData,
|
||||
};
|
||||
|
||||
use crate::Ferese;
|
||||
@@ -21,12 +19,7 @@ impl XdgActivationHandler for Ferese {
|
||||
data.serial.is_some() && data.timestamp.elapsed() <= TOKEN_MAX_AGE
|
||||
}
|
||||
|
||||
fn request_activation(
|
||||
&mut self,
|
||||
token: XdgActivationToken,
|
||||
data: XdgActivationTokenData,
|
||||
surface: WlSurface,
|
||||
) {
|
||||
fn request_activation(&mut self, token: XdgActivationToken, data: XdgActivationTokenData, surface: WlSurface) {
|
||||
if self.session_lock.active || self.input_capture.captures(1) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,28 +1,23 @@
|
||||
use smithay::{
|
||||
backend::{allocator::dmabuf::Dmabuf, renderer::utils::on_commit_buffer_handler},
|
||||
reexports::wayland_server::{
|
||||
Client,
|
||||
protocol::{wl_buffer, wl_surface::WlSurface},
|
||||
},
|
||||
wayland::{
|
||||
buffer::BufferHandler,
|
||||
compositor::{
|
||||
CompositorClientState, CompositorHandler, CompositorState, get_parent,
|
||||
is_sync_subsurface,
|
||||
},
|
||||
dmabuf::{DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier},
|
||||
shm::{ShmHandler, ShmState},
|
||||
},
|
||||
use smithay::backend::allocator::dmabuf::Dmabuf;
|
||||
use smithay::backend::renderer::utils::on_commit_buffer_handler;
|
||||
use smithay::reexports::wayland_server::Client;
|
||||
use smithay::reexports::wayland_server::protocol::wl_buffer;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::wayland::buffer::BufferHandler;
|
||||
use smithay::wayland::compositor::{
|
||||
CompositorClientState, CompositorHandler, CompositorState, get_parent, is_sync_subsurface,
|
||||
};
|
||||
use smithay::wayland::dmabuf::{DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier};
|
||||
use smithay::wayland::shm::{ShmHandler, ShmState};
|
||||
|
||||
use super::{layer_shell, xdg_shell};
|
||||
use crate::{Ferese, state::ClientState};
|
||||
use crate::Ferese;
|
||||
use crate::state::ClientState;
|
||||
|
||||
fn layer_affects_backdrop(layer: smithay::wayland::shell::wlr_layer::Layer) -> bool {
|
||||
matches!(
|
||||
layer,
|
||||
smithay::wayland::shell::wlr_layer::Layer::Background
|
||||
| smithay::wayland::shell::wlr_layer::Layer::Bottom
|
||||
smithay::wayland::shell::wlr_layer::Layer::Background | smithay::wayland::shell::wlr_layer::Layer::Bottom
|
||||
)
|
||||
}
|
||||
|
||||
@@ -68,11 +63,7 @@ impl CompositorHandler for Ferese {
|
||||
let window = {
|
||||
self.space
|
||||
.elements()
|
||||
.find(|window| {
|
||||
window
|
||||
.toplevel()
|
||||
.is_some_and(|toplevel| toplevel.wl_surface() == &root)
|
||||
})
|
||||
.find(|window| window.toplevel().is_some_and(|toplevel| toplevel.wl_surface() == &root))
|
||||
.cloned()
|
||||
};
|
||||
|
||||
@@ -124,12 +115,7 @@ impl DmabufHandler for Ferese {
|
||||
&mut self.dmabuf_state
|
||||
}
|
||||
|
||||
fn dmabuf_imported(
|
||||
&mut self,
|
||||
_global: &DmabufGlobal,
|
||||
dmabuf: Dmabuf,
|
||||
notifier: ImportNotifier,
|
||||
) {
|
||||
fn dmabuf_imported(&mut self, _global: &DmabufGlobal, dmabuf: Dmabuf, notifier: ImportNotifier) {
|
||||
self.queue_dmabuf_import(dmabuf, notifier);
|
||||
crate::backends::direct::render_all(self);
|
||||
}
|
||||
@@ -143,8 +129,9 @@ impl ShmHandler for Ferese {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use smithay::wayland::shell::wlr_layer::Layer;
|
||||
|
||||
use super::*;
|
||||
#[test]
|
||||
fn panel_commits_do_not_invalidate_their_own_backdrop() {
|
||||
assert!(!layer_affects_backdrop(Layer::Top));
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
use smithay::{
|
||||
desktop::{PopupKind, WindowSurfaceType, layer_map_for_output},
|
||||
reexports::wayland_server::protocol::wl_surface::WlSurface,
|
||||
utils::{Logical, Rectangle},
|
||||
wayland::input_method::{InputMethodHandler, PopupSurface},
|
||||
};
|
||||
use smithay::desktop::{PopupKind, WindowSurfaceType, layer_map_for_output};
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::utils::{Logical, Rectangle};
|
||||
use smithay::wayland::input_method::{InputMethodHandler, PopupSurface};
|
||||
|
||||
use crate::Ferese;
|
||||
|
||||
impl InputMethodHandler for Ferese {
|
||||
fn new_popup(&mut self, surface: PopupSurface) {
|
||||
if let Err(error) = self
|
||||
.popups
|
||||
.track_popup(PopupKind::InputMethod(surface.clone()))
|
||||
{
|
||||
if let Err(error) = self.popups.track_popup(PopupKind::InputMethod(surface.clone())) {
|
||||
tracing::warn!(?error, "failed to track input method popup");
|
||||
}
|
||||
crate::backends::direct::render_all(self);
|
||||
@@ -38,10 +33,7 @@ impl InputMethodHandler for Ferese {
|
||||
return geometry;
|
||||
}
|
||||
|
||||
let Some(layer) = self
|
||||
.space
|
||||
.layer_for_surface(parent, WindowSurfaceType::TOPLEVEL)
|
||||
else {
|
||||
let Some(layer) = self.space.layer_for_surface(parent, WindowSurfaceType::TOPLEVEL) else {
|
||||
return Rectangle::default();
|
||||
};
|
||||
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
use smithay::{
|
||||
desktop::{
|
||||
LayerSurface as DesktopLayerSurface, PopupKind, WindowSurfaceType, layer_map_for_output,
|
||||
},
|
||||
output::Output,
|
||||
reexports::wayland_server::protocol::{wl_output::WlOutput, wl_surface::WlSurface},
|
||||
wayland::shell::{
|
||||
wlr_layer::{
|
||||
KeyboardInteractivity, Layer, LayerSurface, WlrLayerShellHandler, WlrLayerShellState,
|
||||
},
|
||||
xdg::PopupSurface,
|
||||
},
|
||||
use smithay::desktop::{LayerSurface as DesktopLayerSurface, PopupKind, WindowSurfaceType, layer_map_for_output};
|
||||
use smithay::output::Output;
|
||||
use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::wayland::shell::wlr_layer::{
|
||||
KeyboardInteractivity, Layer, LayerSurface, WlrLayerShellHandler, WlrLayerShellState,
|
||||
};
|
||||
use smithay::wayland::shell::xdg::PopupSurface;
|
||||
|
||||
use crate::Ferese;
|
||||
|
||||
@@ -19,13 +14,7 @@ impl WlrLayerShellHandler for Ferese {
|
||||
&mut self.layer_shell_state
|
||||
}
|
||||
|
||||
fn new_layer_surface(
|
||||
&mut self,
|
||||
surface: LayerSurface,
|
||||
output: Option<WlOutput>,
|
||||
_layer: Layer,
|
||||
namespace: String,
|
||||
) {
|
||||
fn new_layer_surface(&mut self, surface: LayerSurface, output: Option<WlOutput>, _layer: Layer, namespace: String) {
|
||||
let output = output
|
||||
.as_ref()
|
||||
.and_then(Output::from_resource)
|
||||
@@ -42,10 +31,7 @@ impl WlrLayerShellHandler for Ferese {
|
||||
}
|
||||
|
||||
tracing::debug!(%namespace, output = %output.name(), "mapped layer surface");
|
||||
super::set_surface_tree_scale(
|
||||
layer.wl_surface(),
|
||||
output.current_scale().fractional_scale(),
|
||||
);
|
||||
super::set_surface_tree_scale(layer.wl_surface(), output.current_scale().fractional_scale());
|
||||
self.relayout();
|
||||
crate::backends::direct::render_all(self);
|
||||
}
|
||||
@@ -89,10 +75,7 @@ impl WlrLayerShellHandler for Ferese {
|
||||
}
|
||||
|
||||
pub fn handle_commit(state: &mut Ferese, surface: &WlSurface) {
|
||||
let Some(layer) = state
|
||||
.space
|
||||
.layer_for_surface(surface, WindowSurfaceType::TOPLEVEL)
|
||||
else {
|
||||
let Some(layer) = state.space.layer_for_surface(surface, WindowSurfaceType::TOPLEVEL) else {
|
||||
return;
|
||||
};
|
||||
let outputs = state.space.outputs().cloned().collect::<Vec<_>>();
|
||||
@@ -112,15 +95,11 @@ pub fn handle_commit(state: &mut Ferese, surface: &WlSurface) {
|
||||
&& policy.keyboard_interactivity == KeyboardInteractivity::Exclusive
|
||||
&& matches!(policy.layer, Layer::Top | Layer::Overlay)
|
||||
{
|
||||
state
|
||||
.seat
|
||||
.get_keyboard()
|
||||
.expect("seat has a keyboard")
|
||||
.set_focus(
|
||||
state,
|
||||
Some(layer.wl_surface().clone()),
|
||||
smithay::utils::SERIAL_COUNTER.next_serial(),
|
||||
);
|
||||
state.seat.get_keyboard().expect("seat has a keyboard").set_focus(
|
||||
state,
|
||||
Some(layer.wl_surface().clone()),
|
||||
smithay::utils::SERIAL_COUNTER.next_serial(),
|
||||
);
|
||||
} else if !layer.can_receive_keyboard_focus() && layer_has_keyboard_focus(state, &layer) {
|
||||
state.restore_keyboard_focus();
|
||||
}
|
||||
@@ -136,10 +115,6 @@ fn layer_has_keyboard_focus(state: &Ferese, layer: &DesktopLayerSurface) -> bool
|
||||
.seat
|
||||
.get_keyboard()
|
||||
.and_then(|keyboard| keyboard.current_focus())
|
||||
.and_then(|surface| {
|
||||
state
|
||||
.space
|
||||
.layer_for_surface(&surface, WindowSurfaceType::ALL)
|
||||
})
|
||||
.and_then(|surface| state.space.layer_for_surface(&surface, WindowSurfaceType::ALL))
|
||||
.is_some_and(|focused| focused == *layer)
|
||||
}
|
||||
@@ -8,42 +8,31 @@ pub(crate) mod screenshot_worker;
|
||||
pub(crate) mod window_capture;
|
||||
mod xdg_shell;
|
||||
|
||||
use smithay::{
|
||||
desktop::{PopupManager, layer_map_for_output},
|
||||
input::{
|
||||
Seat, SeatHandler, SeatState,
|
||||
pointer::{CursorImageStatus, PointerHandle},
|
||||
},
|
||||
reexports::wayland_server::{Resource, protocol::wl_surface::WlSurface},
|
||||
utils::Point,
|
||||
wayland::{
|
||||
compositor::{TraversalAction, with_states, with_surface_tree_downward},
|
||||
fractional_scale::{FractionalScaleHandler, with_fractional_scale},
|
||||
idle_inhibit::IdleInhibitHandler,
|
||||
idle_notify::{IdleNotifierHandler, IdleNotifierState},
|
||||
keyboard_shortcuts_inhibit::{
|
||||
KeyboardShortcutsInhibitHandler, KeyboardShortcutsInhibitState,
|
||||
KeyboardShortcutsInhibitor, KeyboardShortcutsInhibitorSeat,
|
||||
},
|
||||
output::OutputHandler,
|
||||
pointer_constraints::{
|
||||
PointerConstraint, PointerConstraintsHandler, with_pointer_constraint,
|
||||
},
|
||||
selection::{
|
||||
SelectionHandler,
|
||||
data_device::{
|
||||
ClientDndGrabHandler, DataDeviceHandler, DataDeviceState, ServerDndGrabHandler,
|
||||
set_data_device_focus,
|
||||
},
|
||||
primary_selection::{
|
||||
PrimarySelectionHandler, PrimarySelectionState, set_primary_focus,
|
||||
},
|
||||
},
|
||||
tablet_manager::TabletSeatHandler,
|
||||
xdg_foreign::{XdgForeignHandler, XdgForeignState},
|
||||
xdg_toplevel_icon::XdgToplevelIconHandler,
|
||||
},
|
||||
use smithay::desktop::{PopupManager, layer_map_for_output};
|
||||
use smithay::input::pointer::{CursorImageStatus, PointerHandle};
|
||||
use smithay::input::{Seat, SeatHandler, SeatState};
|
||||
use smithay::reexports::wayland_server::Resource;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::wayland::compositor::{TraversalAction, with_states, with_surface_tree_downward};
|
||||
use smithay::wayland::fractional_scale::{FractionalScaleHandler, with_fractional_scale};
|
||||
use smithay::wayland::idle_inhibit::IdleInhibitHandler;
|
||||
use smithay::wayland::idle_notify::{IdleNotifierHandler, IdleNotifierState};
|
||||
use smithay::wayland::keyboard_shortcuts_inhibit::{
|
||||
KeyboardShortcutsInhibitHandler, KeyboardShortcutsInhibitState, KeyboardShortcutsInhibitor,
|
||||
KeyboardShortcutsInhibitorSeat,
|
||||
};
|
||||
use smithay::wayland::output::OutputHandler;
|
||||
use smithay::wayland::pointer_constraints::{PointerConstraint, PointerConstraintsHandler, with_pointer_constraint};
|
||||
use smithay::wayland::selection::SelectionHandler;
|
||||
use smithay::wayland::selection::data_device::{
|
||||
ClientDndGrabHandler, DataDeviceHandler, DataDeviceState, ServerDndGrabHandler, set_data_device_focus,
|
||||
};
|
||||
use smithay::wayland::selection::primary_selection::{
|
||||
PrimarySelectionHandler, PrimarySelectionState, set_primary_focus,
|
||||
};
|
||||
use smithay::wayland::tablet_manager::TabletSeatHandler;
|
||||
use smithay::wayland::xdg_foreign::{XdgForeignHandler, XdgForeignState};
|
||||
use smithay::wayland::xdg_toplevel_icon::XdgToplevelIconHandler;
|
||||
|
||||
use crate::Ferese;
|
||||
|
||||
@@ -75,9 +64,7 @@ impl SeatHandler for Ferese {
|
||||
{
|
||||
inhibitor.inactivate();
|
||||
}
|
||||
if let Some(inhibitor) =
|
||||
focused.and_then(|surface| seat.keyboard_shortcuts_inhibitor_for_surface(surface))
|
||||
{
|
||||
if let Some(inhibitor) = focused.and_then(|surface| seat.keyboard_shortcuts_inhibitor_for_surface(surface)) {
|
||||
inhibitor.activate();
|
||||
self.active_shortcuts_inhibitor = Some(inhibitor);
|
||||
}
|
||||
@@ -137,10 +124,7 @@ impl KeyboardShortcutsInhibitHandler for Ferese {
|
||||
}
|
||||
|
||||
fn new_inhibitor(&mut self, inhibitor: KeyboardShortcutsInhibitor) {
|
||||
let focused = self
|
||||
.seat
|
||||
.get_keyboard()
|
||||
.and_then(|keyboard| keyboard.current_focus());
|
||||
let focused = self.seat.get_keyboard().and_then(|keyboard| keyboard.current_focus());
|
||||
|
||||
if focused.as_ref() == Some(inhibitor.wl_surface()) {
|
||||
inhibitor.activate();
|
||||
@@ -199,7 +183,14 @@ pub(crate) fn set_surface_tree_scale(surface: &WlSurface, scale: f64) {
|
||||
}
|
||||
|
||||
impl PointerConstraintsHandler for Ferese {
|
||||
fn new_constraint(&mut self, _surface: &WlSurface, pointer: &PointerHandle<Self>) {
|
||||
fn new_constraint(&mut self, surface: &WlSurface, pointer: &PointerHandle<Self>) {
|
||||
if self
|
||||
.locked_pointer_hint
|
||||
.as_ref()
|
||||
.is_some_and(|hint| hint.surface == *surface)
|
||||
{
|
||||
self.locked_pointer_hint = None;
|
||||
}
|
||||
self.activate_focused_pointer_constraint(pointer);
|
||||
}
|
||||
|
||||
@@ -218,20 +209,10 @@ impl PointerConstraintsHandler for Ferese {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some((focused_surface, origin)) = self.surface_under(pointer.current_location()) else {
|
||||
return;
|
||||
};
|
||||
if focused_surface == *surface {
|
||||
let current = pointer.current_location();
|
||||
let (scale_x, scale_y) = self
|
||||
.window_under_visual(current)
|
||||
.and_then(|window| self.visual_scale_for_window(&window))
|
||||
.unwrap_or((1.0, 1.0));
|
||||
let current_surface_location = current - origin;
|
||||
let offset = location - current_surface_location;
|
||||
|
||||
pointer.set_location(current + Point::from((offset.x * scale_x, offset.y * scale_y)));
|
||||
}
|
||||
self.locked_pointer_hint = Some(crate::input::LockedPointerHint {
|
||||
surface: surface.clone(),
|
||||
local: location,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,27 +1,21 @@
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use super::screenshot::{CaptureBuffer, PartOutcome, PartSender};
|
||||
use smithay::{
|
||||
backend::{
|
||||
allocator::Fourcc,
|
||||
renderer::{ExportMem, TextureMapping},
|
||||
},
|
||||
output::Output,
|
||||
reexports::{
|
||||
wayland_protocols_wlr::screencopy::v1::server::{
|
||||
zwlr_screencopy_frame_v1::{self, ZwlrScreencopyFrameV1},
|
||||
zwlr_screencopy_manager_v1::{self, ZwlrScreencopyManagerV1},
|
||||
},
|
||||
wayland_server::{
|
||||
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
|
||||
protocol::{wl_buffer::WlBuffer, wl_shm},
|
||||
},
|
||||
},
|
||||
utils::{Buffer, Logical, Rectangle, Size},
|
||||
wayland::shm::with_buffer_contents_mut,
|
||||
use smithay::backend::allocator::Fourcc;
|
||||
use smithay::backend::renderer::{ExportMem, TextureMapping};
|
||||
use smithay::output::Output;
|
||||
use smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::zwlr_screencopy_frame_v1::ZwlrScreencopyFrameV1;
|
||||
use smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1;
|
||||
use smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::{
|
||||
zwlr_screencopy_frame_v1, zwlr_screencopy_manager_v1,
|
||||
};
|
||||
use smithay::reexports::wayland_server::protocol::wl_buffer::WlBuffer;
|
||||
use smithay::reexports::wayland_server::protocol::wl_shm;
|
||||
use smithay::reexports::wayland_server::{Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource};
|
||||
use smithay::utils::{Buffer, Logical, Rectangle, Size};
|
||||
use smithay::wayland::shm::with_buffer_contents_mut;
|
||||
|
||||
use super::screenshot::{CaptureBuffer, PartOutcome, PartSender};
|
||||
use crate::Ferese;
|
||||
|
||||
const BYTES_PER_PIXEL: usize = 4;
|
||||
@@ -150,8 +144,7 @@ impl PendingScreencopy {
|
||||
|
||||
pub(crate) fn capture_allowed() -> bool {
|
||||
static ALLOWED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
|
||||
*ALLOWED
|
||||
.get_or_init(|| capture_opted_in(std::env::var_os("FERESE_ENABLE_SCREENCOPY").as_deref()))
|
||||
*ALLOWED.get_or_init(|| capture_opted_in(std::env::var_os("FERESE_ENABLE_SCREENCOPY").as_deref()))
|
||||
}
|
||||
|
||||
fn capture_opted_in(value: Option<&std::ffi::OsStr>) -> bool {
|
||||
@@ -312,9 +305,7 @@ impl Ferese {
|
||||
let captures = std::mem::take(&mut self.pending_screencopies);
|
||||
|
||||
for capture in captures {
|
||||
if &capture.output != output
|
||||
|| !cursor_overlay_matches(capture.overlay_cursor, overlay_cursor)
|
||||
{
|
||||
if &capture.output != output || !cursor_overlay_matches(capture.overlay_cursor, overlay_cursor) {
|
||||
remaining.push(capture);
|
||||
continue;
|
||||
}
|
||||
@@ -325,8 +316,7 @@ impl Ferese {
|
||||
}
|
||||
|
||||
framebuffer_binding_changed = true;
|
||||
if complete_capture(renderer, framebuffer, &capture, self.start_time.elapsed()).is_err()
|
||||
{
|
||||
if complete_capture(renderer, framebuffer, &capture, self.start_time.elapsed()).is_err() {
|
||||
capture.fail();
|
||||
}
|
||||
}
|
||||
@@ -336,10 +326,9 @@ impl Ferese {
|
||||
}
|
||||
|
||||
pub(crate) fn has_pending_screencopy(&self, output: &Output, overlay_cursor: bool) -> bool {
|
||||
self.pending_screencopies.iter().any(|capture| {
|
||||
capture.output == *output
|
||||
&& cursor_overlay_matches(capture.overlay_cursor, overlay_cursor)
|
||||
})
|
||||
self.pending_screencopies
|
||||
.iter()
|
||||
.any(|capture| capture.output == *output && cursor_overlay_matches(capture.overlay_cursor, overlay_cursor))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -404,10 +393,7 @@ fn create_frame(
|
||||
}
|
||||
}
|
||||
|
||||
fn capture_region(
|
||||
output: &Output,
|
||||
requested: Option<Rectangle<i32, Logical>>,
|
||||
) -> Option<Rectangle<i32, Buffer>> {
|
||||
fn capture_region(output: &Output, requested: Option<Rectangle<i32, Logical>>) -> Option<Rectangle<i32, Buffer>> {
|
||||
let mode = output.current_mode()?;
|
||||
let scale = output.current_scale().fractional_scale();
|
||||
let transform = output.current_transform();
|
||||
@@ -434,9 +420,7 @@ pub(crate) fn capture_region_for_geometry(
|
||||
) -> Option<Rectangle<i32, Buffer>> {
|
||||
let logical_size = output_logical_size(mode_size, scale, transform);
|
||||
let output_region = Rectangle::from_size(logical_size);
|
||||
let logical_region = requested
|
||||
.unwrap_or(output_region)
|
||||
.intersection(output_region)?;
|
||||
let logical_region = requested.unwrap_or(output_region).intersection(output_region)?;
|
||||
|
||||
if logical_region.size.w <= 0 || logical_region.size.h <= 0 {
|
||||
return None;
|
||||
@@ -488,9 +472,7 @@ where
|
||||
tracing::warn!(?error, "failed to map output framebuffer copy");
|
||||
})?;
|
||||
let expected = capture.region.size;
|
||||
let stride = (expected.w as usize)
|
||||
.checked_mul(BYTES_PER_PIXEL)
|
||||
.ok_or(())?;
|
||||
let stride = (expected.w as usize).checked_mul(BYTES_PER_PIXEL).ok_or(())?;
|
||||
let bytes = stride.checked_mul(expected.h as usize).ok_or(())?;
|
||||
if source.len() < bytes {
|
||||
return Err(());
|
||||
@@ -548,20 +530,17 @@ where
|
||||
frame.flags(zwlr_screencopy_frame_v1::Flags::empty());
|
||||
|
||||
let seconds = timestamp.as_secs();
|
||||
frame.ready(
|
||||
(seconds >> 32) as u32,
|
||||
seconds as u32,
|
||||
timestamp.subsec_nanos(),
|
||||
);
|
||||
frame.ready((seconds >> 32) as u32, seconds as u32, timestamp.subsec_nanos());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use smithay::utils::{Physical, Transform};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn capture_is_off_unless_the_opt_in_is_exactly_one() {
|
||||
use std::ffi::OsStr;
|
||||
@@ -580,8 +559,7 @@ mod tests {
|
||||
let mode = Size::<i32, Physical>::from((1_920, 1_080));
|
||||
let requested = Rectangle::new((-10, 20).into(), (110, 50).into());
|
||||
|
||||
let region =
|
||||
capture_region_for_geometry(mode, 2.0, Transform::Normal, Some(requested)).unwrap();
|
||||
let region = capture_region_for_geometry(mode, 2.0, Transform::Normal, Some(requested)).unwrap();
|
||||
|
||||
assert_eq!(region, Rectangle::new((0, 40).into(), (200, 100).into()));
|
||||
}
|
||||
@@ -591,9 +569,7 @@ mod tests {
|
||||
let mode = Size::<i32, Physical>::from((1_920, 1_080));
|
||||
let requested = Rectangle::new((1_000, 600).into(), (100, 100).into());
|
||||
|
||||
assert!(
|
||||
capture_region_for_geometry(mode, 2.0, Transform::Normal, Some(requested),).is_none()
|
||||
);
|
||||
assert!(capture_region_for_geometry(mode, 2.0, Transform::Normal, Some(requested),).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -36,9 +36,7 @@ const MAX_CANVAS_BYTES: usize = 128 * 1024 * 1024;
|
||||
|
||||
impl Canvas {
|
||||
fn new(width: u32, height: u32) -> Option<Self> {
|
||||
let length = (width as usize)
|
||||
.checked_mul(height as usize)?
|
||||
.checked_mul(4)?;
|
||||
let length = (width as usize).checked_mul(height as usize)?.checked_mul(4)?;
|
||||
if width == 0 || height == 0 || length > MAX_CANVAS_BYTES {
|
||||
return None;
|
||||
}
|
||||
@@ -81,11 +79,7 @@ impl Canvas {
|
||||
}
|
||||
}
|
||||
|
||||
fn scale_and_convert(
|
||||
frame: &OutputFrame,
|
||||
target_width: usize,
|
||||
target_height: usize,
|
||||
) -> Option<Vec<u8>> {
|
||||
fn scale_and_convert(frame: &OutputFrame, target_width: usize, target_height: usize) -> Option<Vec<u8>> {
|
||||
let (upright_width, upright_height) = upright_size(frame);
|
||||
if upright_width <= 0 || upright_height <= 0 || target_width == 0 || target_height == 0 {
|
||||
return None;
|
||||
@@ -97,13 +91,8 @@ fn scale_and_convert(
|
||||
if upright_width as usize == target_width && upright_height as usize == target_height {
|
||||
for row in 0..target_height {
|
||||
for column in 0..target_width {
|
||||
let (x, y) = upright_source_pixel(
|
||||
column as i32,
|
||||
row as i32,
|
||||
frame.width,
|
||||
frame.height,
|
||||
frame.transform,
|
||||
);
|
||||
let (x, y) =
|
||||
upright_source_pixel(column as i32, row as i32, frame.width, frame.height, frame.transform);
|
||||
let from = (y as usize)
|
||||
.checked_mul(frame.stride)?
|
||||
.checked_add(x as usize * BYTES_PER_PIXEL)?;
|
||||
@@ -154,13 +143,7 @@ fn scale_and_convert(
|
||||
Some(out)
|
||||
}
|
||||
|
||||
fn upright_source_pixel(
|
||||
x: i32,
|
||||
y: i32,
|
||||
width: i32,
|
||||
height: i32,
|
||||
transform: Transform,
|
||||
) -> (i32, i32) {
|
||||
fn upright_source_pixel(x: i32, y: i32, width: i32, height: i32, transform: Transform) -> (i32, i32) {
|
||||
match transform {
|
||||
Transform::_90 => (y, height - 1 - x),
|
||||
Transform::_180 => (width - 1 - x, height - 1 - y),
|
||||
@@ -186,10 +169,7 @@ pub(crate) fn compose(frames: &[OutputFrame]) -> Result<Canvas, String> {
|
||||
}
|
||||
|
||||
let (left, top) = logical_origin(frames).ok_or("Screenshot region is empty")?;
|
||||
let scale = frames
|
||||
.iter()
|
||||
.map(|frame| frame.scale)
|
||||
.fold(0.0_f64, f64::max);
|
||||
let scale = frames.iter().map(|frame| frame.scale).fold(0.0_f64, f64::max);
|
||||
if !(scale.is_finite() && scale > 0.0) {
|
||||
return Err("Screenshot outputs have an invalid scale".into());
|
||||
}
|
||||
@@ -216,22 +196,15 @@ pub(crate) fn compose(frames: &[OutputFrame]) -> Result<Canvas, String> {
|
||||
if width <= 0 || height <= 0 {
|
||||
return Err("Screenshot region is empty".into());
|
||||
}
|
||||
let mut canvas = Canvas::new(width as u32, height as u32)
|
||||
.ok_or("Screenshot image is too large to encode")?;
|
||||
let mut canvas = Canvas::new(width as u32, height as u32).ok_or("Screenshot image is too large to encode")?;
|
||||
|
||||
for (frame, (x0, y0, target_width, target_height)) in frames.iter().zip(placement) {
|
||||
if target_width == 0 || target_height == 0 {
|
||||
continue;
|
||||
}
|
||||
let scaled = scale_and_convert(frame, target_width as usize, target_height as usize)
|
||||
.ok_or("Invalid capture size")?;
|
||||
canvas.blit(
|
||||
&scaled,
|
||||
target_width as usize,
|
||||
target_height as usize,
|
||||
x0,
|
||||
y0,
|
||||
);
|
||||
let scaled =
|
||||
scale_and_convert(frame, target_width as usize, target_height as usize).ok_or("Invalid capture size")?;
|
||||
canvas.blit(&scaled, target_width as usize, target_height as usize, x0, y0);
|
||||
}
|
||||
|
||||
Ok(canvas)
|
||||
@@ -292,12 +265,7 @@ fn logical_origin(frames: &[OutputFrame]) -> Option<(i32, i32)> {
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum Geometry {
|
||||
All,
|
||||
Region {
|
||||
x: i32,
|
||||
y: i32,
|
||||
width: i32,
|
||||
height: i32,
|
||||
},
|
||||
Region { x: i32, y: i32, width: i32, height: i32 },
|
||||
}
|
||||
|
||||
impl Geometry {
|
||||
@@ -307,16 +275,9 @@ impl Geometry {
|
||||
if width <= 0 || height <= 0 {
|
||||
return Err("Screenshot region must have a positive size".into());
|
||||
}
|
||||
x.checked_add(width)
|
||||
.ok_or("screenshot geometry overflows")?;
|
||||
y.checked_add(height)
|
||||
.ok_or("screenshot geometry overflows")?;
|
||||
Ok(Geometry::Region {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
})
|
||||
x.checked_add(width).ok_or("screenshot geometry overflows")?;
|
||||
y.checked_add(height).ok_or("screenshot geometry overflows")?;
|
||||
Ok(Geometry::Region { x, y, width, height })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -394,10 +355,7 @@ struct Request {
|
||||
|
||||
pub(crate) enum Action {
|
||||
None,
|
||||
Encode {
|
||||
request: u64,
|
||||
frames: Vec<OutputFrame>,
|
||||
},
|
||||
Encode { request: u64, frames: Vec<OutputFrame> },
|
||||
// The path was already sent to the caller; it must not be cleaned up.
|
||||
Delivered(PathBuf),
|
||||
// The request is gone, so this file is ours to remove.
|
||||
@@ -461,10 +419,7 @@ impl Coordinator {
|
||||
response_id,
|
||||
reply,
|
||||
state: RequestState::Collecting,
|
||||
parts: specs
|
||||
.into_iter()
|
||||
.map(|spec| (spec, PartState::Pending))
|
||||
.collect(),
|
||||
parts: specs.into_iter().map(|spec| (spec, PartState::Pending)).collect(),
|
||||
received: 0,
|
||||
deadline: Instant::now() + REQUEST_TIMEOUT,
|
||||
},
|
||||
@@ -485,12 +440,7 @@ impl Coordinator {
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn on_part(
|
||||
&mut self,
|
||||
request: u64,
|
||||
part: usize,
|
||||
result: Result<CaptureBuffer, String>,
|
||||
) -> Action {
|
||||
pub(crate) fn on_part(&mut self, request: u64, part: usize, result: Result<CaptureBuffer, String>) -> Action {
|
||||
let Some(entry) = self.requests.get_mut(&request) else {
|
||||
return Action::None;
|
||||
};
|
||||
@@ -595,10 +545,7 @@ impl Coordinator {
|
||||
.map(|(id, _)| *id)
|
||||
.collect();
|
||||
for id in &expired {
|
||||
self.reject_inner(
|
||||
*id,
|
||||
"Screenshot timed out: an output was not redrawn in time",
|
||||
);
|
||||
self.reject_inner(*id, "Screenshot timed out: an output was not redrawn in time");
|
||||
}
|
||||
expired
|
||||
}
|
||||
@@ -682,10 +629,7 @@ impl PlannedPart {
|
||||
// Turns a requested logical region into the per-output reads that cover it.
|
||||
// The logical-to-buffer conversion is shared with the screencopy path so both
|
||||
// agree on rounding at fractional scales.
|
||||
pub(crate) fn plan(
|
||||
geometry: &Geometry,
|
||||
outputs: &[OutputLayout],
|
||||
) -> Result<Vec<PlannedPart>, String> {
|
||||
pub(crate) fn plan(geometry: &Geometry, outputs: &[OutputLayout]) -> Result<Vec<PlannedPart>, String> {
|
||||
if outputs.is_empty() {
|
||||
return Err("No outputs are enabled".into());
|
||||
}
|
||||
@@ -696,8 +640,7 @@ pub(crate) fn plan(
|
||||
{
|
||||
return Err("Screenshot region must have a positive size".into());
|
||||
}
|
||||
let logical_sizes: Vec<Size<i32, Logical>> =
|
||||
outputs.iter().map(OutputLayout::logical_size).collect();
|
||||
let logical_sizes: Vec<Size<i32, Logical>> = outputs.iter().map(OutputLayout::logical_size).collect();
|
||||
|
||||
let requested = match geometry {
|
||||
Geometry::All => {
|
||||
@@ -709,10 +652,8 @@ pub(crate) fn plan(
|
||||
};
|
||||
union = Some(match union {
|
||||
Some(current) => {
|
||||
let top_left = Point::<i32, Logical>::new(
|
||||
current.loc.x.min(rect.loc.x),
|
||||
current.loc.y.min(rect.loc.y),
|
||||
);
|
||||
let top_left =
|
||||
Point::<i32, Logical>::new(current.loc.x.min(rect.loc.x), current.loc.y.min(rect.loc.y));
|
||||
let bottom_right = Point::<i32, Logical>::new(
|
||||
(current.loc.x + current.size.w).max(rect.loc.x + rect.size.w),
|
||||
(current.loc.y + current.size.h).max(rect.loc.y + rect.size.h),
|
||||
@@ -724,12 +665,7 @@ pub(crate) fn plan(
|
||||
}
|
||||
union.expect("outputs is not empty")
|
||||
}
|
||||
Geometry::Region {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
} => Rectangle {
|
||||
Geometry::Region { x, y, width, height } => Rectangle {
|
||||
loc: (*x, *y).into(),
|
||||
size: (*width, *height).into(),
|
||||
},
|
||||
@@ -757,11 +693,7 @@ pub(crate) fn plan(
|
||||
}
|
||||
// capture_region_for_geometry works in output-local logical space.
|
||||
let local = Rectangle {
|
||||
loc: (
|
||||
clipped.loc.x - output.location.x,
|
||||
clipped.loc.y - output.location.y,
|
||||
)
|
||||
.into(),
|
||||
loc: (clipped.loc.x - output.location.x, clipped.loc.y - output.location.y).into(),
|
||||
size: clipped.size,
|
||||
};
|
||||
// The clip above already intersected against this same logical rect, so
|
||||
@@ -808,34 +740,19 @@ mod planning {
|
||||
}
|
||||
|
||||
fn region(x: i32, y: i32, width: i32, height: i32) -> Geometry {
|
||||
Geometry::Region {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
}
|
||||
Geometry::Region { x, y, width, height }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_crop_crossing_a_seam_splits_into_one_part_per_output() {
|
||||
let outputs = [
|
||||
output(0, 0, 1920, 1080, 1.0),
|
||||
output(1920, 0, 1920, 1080, 1.0),
|
||||
];
|
||||
let outputs = [output(0, 0, 1920, 1080, 1.0), output(1920, 0, 1920, 1080, 1.0)];
|
||||
let parts = plan(®ion(1800, 0, 200, 100), &outputs).unwrap();
|
||||
assert_eq!(parts.len(), 2);
|
||||
|
||||
assert_eq!(parts[0].index, 0);
|
||||
assert_eq!((parts[0].logical.loc.x, parts[0].logical.size.w), (1800, 120));
|
||||
assert_eq!(
|
||||
(parts[0].logical.loc.x, parts[0].logical.size.w),
|
||||
(1800, 120)
|
||||
);
|
||||
assert_eq!(
|
||||
(
|
||||
parts[1].index,
|
||||
parts[1].logical.loc.x,
|
||||
parts[1].logical.size.w
|
||||
),
|
||||
(parts[1].index, parts[1].logical.loc.x, parts[1].logical.size.w),
|
||||
(1, 1920, 80)
|
||||
);
|
||||
// The clipped widths must add back up to the request.
|
||||
@@ -868,14 +785,8 @@ mod planning {
|
||||
let outputs = [output(0, 0, 800, 600, 1.0), output(800, 0, 1600, 1200, 1.5)];
|
||||
let parts = plan(®ion(600, 0, 400, 300), &outputs).unwrap();
|
||||
assert_eq!(parts.len(), 2);
|
||||
assert_eq!(
|
||||
(parts[0].logical.loc.x, parts[0].logical.size.w),
|
||||
(600, 200)
|
||||
);
|
||||
assert_eq!(
|
||||
(parts[1].logical.loc.x, parts[1].logical.size.w),
|
||||
(800, 200)
|
||||
);
|
||||
assert_eq!((parts[0].logical.loc.x, parts[0].logical.size.w), (600, 200));
|
||||
assert_eq!((parts[1].logical.loc.x, parts[1].logical.size.w), (800, 200));
|
||||
// The 1.5 output reads back at 1.5x while still reporting logical 200.
|
||||
let spec = parts[1].spec(&outputs[1]);
|
||||
assert_eq!(spec.logical_width, 200);
|
||||
@@ -885,10 +796,7 @@ mod planning {
|
||||
|
||||
#[test]
|
||||
fn all_geometry_unions_every_output() {
|
||||
let outputs = [
|
||||
output(0, 0, 1920, 1080, 1.0),
|
||||
output(-1280, -400, 1280, 800, 1.0),
|
||||
];
|
||||
let outputs = [output(0, 0, 1920, 1080, 1.0), output(-1280, -400, 1280, 800, 1.0)];
|
||||
let parts = plan(&Geometry::All, &outputs).unwrap();
|
||||
assert_eq!(parts.len(), 2);
|
||||
// Neither output is moved into a normalised origin at plan time.
|
||||
@@ -899,10 +807,7 @@ mod planning {
|
||||
|
||||
#[test]
|
||||
fn a_crop_misses_outputs_that_do_not_intersect() {
|
||||
let outputs = [
|
||||
output(0, 0, 1920, 1080, 1.0),
|
||||
output(1920, 0, 1920, 1080, 1.0),
|
||||
];
|
||||
let outputs = [output(0, 0, 1920, 1080, 1.0), output(1920, 0, 1920, 1080, 1.0)];
|
||||
let parts = plan(®ion(4000, 0, 100, 100), &outputs);
|
||||
assert!(parts.is_err(), "a region off the desktop is reported");
|
||||
}
|
||||
@@ -912,17 +817,11 @@ mod planning {
|
||||
// The second output starts exactly where the region ends. It contributes
|
||||
// nothing, so the capture must still succeed with just the first output
|
||||
// rather than failing on a region it does not overlap.
|
||||
let outputs = [
|
||||
output(0, 0, 1920, 1080, 1.0),
|
||||
output(1920, 0, 1920, 1080, 1.0),
|
||||
];
|
||||
let outputs = [output(0, 0, 1920, 1080, 1.0), output(1920, 0, 1920, 1080, 1.0)];
|
||||
let parts = plan(®ion(100, 100, 1820, 980), &outputs).expect("captures the overlap");
|
||||
assert_eq!(parts.len(), 1, "only the overlapping output is read");
|
||||
assert_eq!(parts[0].index, 0);
|
||||
assert_eq!(
|
||||
(parts[0].logical.size.w, parts[0].logical.size.h),
|
||||
(1820, 980)
|
||||
);
|
||||
assert_eq!((parts[0].logical.size.w, parts[0].logical.size.h), (1820, 980));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -932,16 +831,10 @@ mod planning {
|
||||
// A 90-degree transform turns the 1920x1080 mode into a 1080x1920
|
||||
// logical area, and the readback follows the same rotation.
|
||||
let parts = plan(®ion(0, 0, 1080, 400), std::slice::from_ref(&rotated)).unwrap();
|
||||
assert_eq!(
|
||||
(parts[0].logical.size.w, parts[0].logical.size.h),
|
||||
(1080, 400)
|
||||
);
|
||||
assert_eq!((parts[0].logical.size.w, parts[0].logical.size.h), (1080, 400));
|
||||
let spec = parts[0].spec(&rotated);
|
||||
assert_eq!(spec.transform, Transform::_90);
|
||||
assert!(
|
||||
spec.buffer_height > spec.buffer_width,
|
||||
"buffer axes are swapped"
|
||||
);
|
||||
assert!(spec.buffer_height > spec.buffer_width, "buffer axes are swapped");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -981,10 +874,12 @@ mod planning {
|
||||
|
||||
#[cfg(test)]
|
||||
mod completion {
|
||||
use std::sync::mpsc::sync_channel;
|
||||
|
||||
use smithay::reexports::calloop::channel::channel as loop_channel;
|
||||
|
||||
use super::*;
|
||||
use crate::handlers::screencopy::CaptureSink;
|
||||
use smithay::reexports::calloop::channel::channel as loop_channel;
|
||||
use std::sync::mpsc::sync_channel;
|
||||
|
||||
fn spec(width: i32, height: i32) -> PartSpec {
|
||||
PartSpec {
|
||||
@@ -1021,10 +916,7 @@ mod completion {
|
||||
coordinator
|
||||
.admit(42, reply.clone(), (0..parts).map(|_| spec(2, 2)).collect())
|
||||
.expect("admitted");
|
||||
Self {
|
||||
coordinator,
|
||||
received,
|
||||
}
|
||||
Self { coordinator, received }
|
||||
}
|
||||
|
||||
fn response(&self) -> Option<Response> {
|
||||
@@ -1041,21 +933,13 @@ mod completion {
|
||||
harness.coordinator.on_part(id, 0, Ok(buffer(2, 2))),
|
||||
Action::None
|
||||
));
|
||||
assert!(
|
||||
harness.coordinator.is_live(id),
|
||||
"still collecting after part 0"
|
||||
);
|
||||
assert!(harness.coordinator.is_live(id), "still collecting after part 0");
|
||||
|
||||
assert!(matches!(
|
||||
harness
|
||||
.coordinator
|
||||
.on_part(id, 1, Err("readback failed".into())),
|
||||
harness.coordinator.on_part(id, 1, Err("readback failed".into())),
|
||||
Action::None
|
||||
));
|
||||
assert!(
|
||||
!harness.coordinator.is_live(id),
|
||||
"one failed part ends the request"
|
||||
);
|
||||
assert!(!harness.coordinator.is_live(id), "one failed part ends the request");
|
||||
|
||||
let response = harness.response().expect("caller is always answered");
|
||||
assert_eq!(response.id, 42);
|
||||
@@ -1070,9 +954,7 @@ mod completion {
|
||||
|
||||
harness.coordinator.on_part(id, 0, Ok(buffer(2, 2)));
|
||||
// Output 1 is removed from the layout, so its pending capture is failed.
|
||||
harness
|
||||
.coordinator
|
||||
.on_part(id, 1, Err("output was removed".into()));
|
||||
harness.coordinator.on_part(id, 1, Err("output was removed".into()));
|
||||
assert!(!harness.coordinator.is_live(id));
|
||||
assert!(harness.response().unwrap().error.is_some());
|
||||
|
||||
@@ -1142,9 +1024,7 @@ mod completion {
|
||||
// Unlocking does not resurrect the request; the file the worker
|
||||
// eventually produces is ours to delete.
|
||||
assert!(!harness.coordinator.is_live(id));
|
||||
let action = harness
|
||||
.coordinator
|
||||
.on_encoded(id, Ok(PathBuf::from("/tmp/late.png")));
|
||||
let action = harness.coordinator.on_encoded(id, Ok(PathBuf::from("/tmp/late.png")));
|
||||
assert!(matches!(action, Action::DiscardFile(ref path) if path.ends_with("late.png")));
|
||||
assert!(harness.response().is_none(), "no late success is delivered");
|
||||
}
|
||||
@@ -1155,9 +1035,7 @@ mod completion {
|
||||
let id = harness.coordinator.next_id;
|
||||
harness.coordinator.on_part(id, 0, Ok(buffer(2, 2)));
|
||||
|
||||
harness
|
||||
.coordinator
|
||||
.on_encoded(id, Ok(PathBuf::from("/tmp/shot.png")));
|
||||
harness.coordinator.on_encoded(id, Ok(PathBuf::from("/tmp/shot.png")));
|
||||
let response = harness.response().expect("success is answered");
|
||||
assert_eq!(response.id, 42);
|
||||
assert!(response.error.is_none());
|
||||
@@ -1165,9 +1043,7 @@ mod completion {
|
||||
|
||||
// A duplicate completion for the same request has nowhere to go, and
|
||||
// its file is an orphan rather than a second delivery.
|
||||
let action = harness
|
||||
.coordinator
|
||||
.on_encoded(id, Ok(PathBuf::from("/tmp/again.png")));
|
||||
let action = harness.coordinator.on_encoded(id, Ok(PathBuf::from("/tmp/again.png")));
|
||||
assert!(matches!(action, Action::DiscardFile(_)));
|
||||
|
||||
// A delivered path is reported separately so the caller cannot
|
||||
@@ -1190,9 +1066,7 @@ mod completion {
|
||||
let id = harness.coordinator.next_id;
|
||||
harness.coordinator.on_part(id, 0, Ok(buffer(2, 2)));
|
||||
|
||||
harness
|
||||
.coordinator
|
||||
.on_encoded(id, Err("encoding failed".into()));
|
||||
harness.coordinator.on_encoded(id, Err("encoding failed".into()));
|
||||
let response = harness.response().expect("failure is answered");
|
||||
assert_eq!(response.error.unwrap().message, "encoding failed");
|
||||
assert!(!harness.coordinator.is_live(id));
|
||||
@@ -1205,23 +1079,13 @@ mod completion {
|
||||
// client would never be answered.
|
||||
let mut harness = Harness::new(2);
|
||||
let id = harness.coordinator.next_id;
|
||||
harness
|
||||
.coordinator
|
||||
.requests
|
||||
.get_mut(&id)
|
||||
.expect("tracked")
|
||||
.deadline = Instant::now() - Duration::from_secs(1);
|
||||
harness.coordinator.requests.get_mut(&id).expect("tracked").deadline = Instant::now() - Duration::from_secs(1);
|
||||
|
||||
assert_eq!(harness.coordinator.expire(), vec![id]);
|
||||
let response = harness.response().expect("the caller is answered");
|
||||
assert_eq!(response.error.as_ref().unwrap().code, "screenshot_failed");
|
||||
assert!(
|
||||
response
|
||||
.error
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.message
|
||||
.contains("timed out"),
|
||||
response.error.as_ref().unwrap().message.contains("timed out"),
|
||||
"the failure explains itself: {:?}",
|
||||
response.error.as_ref().unwrap().message
|
||||
);
|
||||
@@ -1302,11 +1166,7 @@ mod completion {
|
||||
// buffer that was handed off is never replaced.
|
||||
assert!(!sink.publish(buffer(64, 64)));
|
||||
|
||||
let PartOutcome {
|
||||
request,
|
||||
part,
|
||||
result,
|
||||
} = receiver.try_recv().expect("published once");
|
||||
let PartOutcome { request, part, result } = receiver.try_recv().expect("published once");
|
||||
assert_eq!((request, part), (7, 1));
|
||||
assert_eq!(result.unwrap().width, 2);
|
||||
assert!(receiver.try_recv().is_err(), "no second buffer was sent");
|
||||
@@ -1321,11 +1181,7 @@ mod completion {
|
||||
// failure has already been reported.
|
||||
assert!(!sink.publish(buffer(2, 2)));
|
||||
|
||||
let PartOutcome {
|
||||
request,
|
||||
part,
|
||||
result,
|
||||
} = receiver.try_recv().expect("answered");
|
||||
let PartOutcome { request, part, result } = receiver.try_recv().expect("answered");
|
||||
assert_eq!((request, part), (3, 0));
|
||||
assert!(result.is_err());
|
||||
assert!(receiver.try_recv().is_err());
|
||||
@@ -1715,11 +1571,7 @@ mod tests {
|
||||
// 9 logical at scale 1.5.
|
||||
assert_eq!((canvas.width, canvas.height), (14, 5));
|
||||
for column in 0..canvas.width as usize {
|
||||
assert_eq!(
|
||||
canvas.pixels[column * 4 + 3],
|
||||
255,
|
||||
"column {column} has a gap"
|
||||
);
|
||||
assert_eq!(canvas.pixels[column * 4 + 3], 255, "column {column} has a gap");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1751,11 +1603,7 @@ mod tests {
|
||||
|
||||
let canvas = compose(&[left, right]).unwrap();
|
||||
assert_eq!(canvas.width, 5);
|
||||
assert_eq!(
|
||||
&canvas.pixels[2 * 4..3 * 4],
|
||||
&[0, 0, 0, 0],
|
||||
"gap is transparent"
|
||||
);
|
||||
assert_eq!(&canvas.pixels[2 * 4..3 * 4], &[0, 0, 0, 0], "gap is transparent");
|
||||
assert_eq!(canvas.pixels[3], 255);
|
||||
assert_eq!(canvas.pixels[3 * 4 + 3], 255);
|
||||
}
|
||||
@@ -1784,11 +1632,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn geometry_tolerates_surrounding_whitespace_from_slurp() {
|
||||
for raw in [
|
||||
"100,200 800x600\n",
|
||||
" 100,200 800x600 ",
|
||||
"100,200 800x600\r\n",
|
||||
] {
|
||||
for raw in ["100,200 800x600\n", " 100,200 800x600 ", "100,200 800x600\r\n"] {
|
||||
assert_eq!(
|
||||
parse_geometry(raw).unwrap(),
|
||||
Geometry::Region {
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::mpsc::{Receiver, SyncSender, TrySendError, sync_channel};
|
||||
use std::thread;
|
||||
use std::time::{Duration, SystemTime};
|
||||
use std::{fs, io, thread};
|
||||
|
||||
use smithay::reexports::calloop::channel;
|
||||
|
||||
@@ -125,14 +123,8 @@ fn encode_png(canvas: &Canvas) -> Result<Vec<u8>, String> {
|
||||
image::codecs::png::CompressionType::Fast,
|
||||
image::codecs::png::FilterType::Adaptive,
|
||||
);
|
||||
image::ImageEncoder::write_image(
|
||||
encoder,
|
||||
image.as_raw(),
|
||||
width,
|
||||
height,
|
||||
image::ExtendedColorType::Rgba8,
|
||||
)
|
||||
.map_err(|error| format!("Could not encode screenshot: {error}"))?;
|
||||
image::ImageEncoder::write_image(encoder, image.as_raw(), width, height, image::ExtendedColorType::Rgba8)
|
||||
.map_err(|error| format!("Could not encode screenshot: {error}"))?;
|
||||
tracing::debug!(encoded = out.len(), "encoded screenshot");
|
||||
Ok(out)
|
||||
}
|
||||
@@ -161,8 +153,8 @@ fn write_private_file(png: &[u8]) -> Result<PathBuf, String> {
|
||||
}
|
||||
|
||||
fn private_directory() -> Result<PathBuf, String> {
|
||||
let runtime = std::env::var_os("XDG_RUNTIME_DIR")
|
||||
.ok_or("XDG_RUNTIME_DIR is not set, so screenshots cannot be staged")?;
|
||||
let runtime =
|
||||
std::env::var_os("XDG_RUNTIME_DIR").ok_or("XDG_RUNTIME_DIR is not set, so screenshots cannot be staged")?;
|
||||
let root = PathBuf::from(runtime);
|
||||
let directory = root.join("ferese-screenshots");
|
||||
match fs::symlink_metadata(&directory) {
|
||||
@@ -177,9 +169,7 @@ fn private_directory() -> Result<PathBuf, String> {
|
||||
}
|
||||
Err(error) if error.kind() == io::ErrorKind::NotFound => {
|
||||
fs::create_dir(&directory)
|
||||
.and_then(|()| {
|
||||
fs::set_permissions(&directory, fs::Permissions::from_mode(DIR_MODE))
|
||||
})
|
||||
.and_then(|()| fs::set_permissions(&directory, fs::Permissions::from_mode(DIR_MODE)))
|
||||
.map_err(|error| format!("Could not create screenshot directory: {error}"))?;
|
||||
}
|
||||
Err(error) => return Err(format!("Could not inspect screenshot directory: {error}")),
|
||||
@@ -189,9 +179,10 @@ fn private_directory() -> Result<PathBuf, String> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use smithay::utils::Transform;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn frame(width: i32, height: i32) -> OutputFrame {
|
||||
let stride = width as usize * 4;
|
||||
let mut pixels = vec![0u8; stride * height as usize];
|
||||
@@ -237,10 +228,7 @@ mod tests {
|
||||
let path = write_private_file(&png).expect("staged");
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let mode = fs::metadata(&path).unwrap().permissions().mode() & 0o777;
|
||||
assert_eq!(
|
||||
mode, FILE_MODE,
|
||||
"staged screenshots are not group/world readable"
|
||||
);
|
||||
assert_eq!(mode, FILE_MODE, "staged screenshots are not group/world readable");
|
||||
fs::remove_file(&path).unwrap();
|
||||
}
|
||||
|
||||
@@ -254,10 +242,7 @@ mod tests {
|
||||
fs::write(&other, b"not a screenshot").unwrap();
|
||||
|
||||
sweep_stale_files();
|
||||
assert!(
|
||||
fresh.exists(),
|
||||
"a freshly staged screenshot is still in use"
|
||||
);
|
||||
assert!(fresh.exists(), "a freshly staged screenshot is still in use");
|
||||
assert!(other.exists(), "unrelated files are left alone");
|
||||
|
||||
fs::remove_file(&fresh).unwrap();
|
||||
|
||||
@@ -1,27 +1,19 @@
|
||||
use std::{
|
||||
os::unix::fs::MetadataExt,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use ferese_layout::WindowId;
|
||||
use ferese_protocols::window_capture::v1::server::ferese_window_capture_manager_v1::{
|
||||
self as manager, FereseWindowCaptureManagerV1,
|
||||
};
|
||||
use smithay::{
|
||||
reexports::{
|
||||
wayland_protocols_wlr::screencopy::v1::server::zwlr_screencopy_frame_v1::{
|
||||
self as frame, ZwlrScreencopyFrameV1,
|
||||
},
|
||||
wayland_server::{
|
||||
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
|
||||
protocol::{wl_buffer::WlBuffer, wl_shm},
|
||||
},
|
||||
},
|
||||
utils::Rectangle,
|
||||
wayland::shm::with_buffer_contents_mut,
|
||||
};
|
||||
use ferese_protocols::window_capture::v1::server::ferese_window_capture_manager_v1 as manager;
|
||||
use ferese_protocols::window_capture::v1::server::ferese_window_capture_manager_v1::FereseWindowCaptureManagerV1;
|
||||
use smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::zwlr_screencopy_frame_v1 as frame;
|
||||
use smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::zwlr_screencopy_frame_v1::ZwlrScreencopyFrameV1;
|
||||
use smithay::reexports::wayland_server::protocol::wl_buffer::WlBuffer;
|
||||
use smithay::reexports::wayland_server::protocol::wl_shm;
|
||||
use smithay::reexports::wayland_server::{Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource};
|
||||
use smithay::utils::Rectangle;
|
||||
use smithay::wayland::shm::with_buffer_contents_mut;
|
||||
|
||||
use super::{screencopy::FrameData, screenshot::CaptureBuffer};
|
||||
use super::screencopy::FrameData;
|
||||
use super::screenshot::CaptureBuffer;
|
||||
use crate::Ferese;
|
||||
|
||||
const MAX_BYTES: usize = 128 * 1024 * 1024;
|
||||
@@ -42,10 +34,7 @@ struct Permit {
|
||||
impl Permit {
|
||||
fn reserve(budget: &Arc<Mutex<Budget>>, bytes: usize) -> Option<Self> {
|
||||
let mut available = budget.lock().unwrap();
|
||||
if bytes == 0
|
||||
|| bytes > MAX_BYTES.saturating_sub(available.bytes)
|
||||
|| available.frames >= MAX_FRAMES
|
||||
{
|
||||
if bytes == 0 || bytes > MAX_BYTES.saturating_sub(available.bytes) || available.frames >= MAX_FRAMES {
|
||||
return None;
|
||||
}
|
||||
available.bytes += bytes;
|
||||
@@ -79,11 +68,7 @@ pub(crate) struct Global {
|
||||
|
||||
pub(crate) fn is_portal(stream: &std::os::unix::net::UnixStream) -> bool {
|
||||
use std::os::fd::AsRawFd;
|
||||
let mut credentials = libc::ucred {
|
||||
pid: 0,
|
||||
uid: 0,
|
||||
gid: 0,
|
||||
};
|
||||
let mut credentials = libc::ucred { pid: 0, uid: 0, gid: 0 };
|
||||
let mut length = std::mem::size_of_val(&credentials) as libc::socklen_t;
|
||||
// SAFETY: the connected socket is live and both out pointers cover their
|
||||
// initialized stack allocations. SO_PEERCRED identifies the connecting process.
|
||||
@@ -101,33 +86,23 @@ pub(crate) fn is_portal(stream: &std::os::unix::net::UnixStream) -> bool {
|
||||
}
|
||||
let expected = std::env::current_exe()
|
||||
.ok()
|
||||
.and_then(|exe| {
|
||||
exe.parent()
|
||||
.map(|dir| dir.join("xdg-desktop-portal-ferese"))
|
||||
})
|
||||
.and_then(|exe| exe.parent().map(|dir| dir.join("xdg-desktop-portal-ferese")))
|
||||
.and_then(|exe| std::fs::metadata(exe).ok());
|
||||
let actual = std::fs::metadata(format!("/proc/{}/exe", credentials.pid)).ok();
|
||||
matches!((expected, actual), (Some(expected), Some(actual)) if expected.dev() == actual.dev() && expected.ino() == actual.ino())
|
||||
}
|
||||
|
||||
pub(super) fn init_global(display: &DisplayHandle) {
|
||||
display.create_global::<Ferese, FereseWindowCaptureManagerV1, Global>(
|
||||
1,
|
||||
Global {
|
||||
budget: Arc::default(),
|
||||
},
|
||||
);
|
||||
display.create_global::<Ferese, FereseWindowCaptureManagerV1, Global>(1, Global { budget: Arc::default() });
|
||||
}
|
||||
|
||||
impl GlobalDispatch<FereseWindowCaptureManagerV1, Global> for Ferese {
|
||||
fn can_view(client: Client, _: &Global) -> bool {
|
||||
client
|
||||
.get_data::<crate::state::ClientState>()
|
||||
.is_some_and(|state| {
|
||||
state
|
||||
.capabilities
|
||||
.contains(crate::private_client::ClientCapabilities::WINDOW_CAPTURE)
|
||||
})
|
||||
client.get_data::<crate::state::ClientState>().is_some_and(|state| {
|
||||
state
|
||||
.capabilities
|
||||
.contains(crate::private_client::ClientCapabilities::WINDOW_CAPTURE)
|
||||
})
|
||||
}
|
||||
|
||||
fn bind(
|
||||
@@ -226,8 +201,7 @@ impl Ferese {
|
||||
.clone();
|
||||
let geometry = window.geometry();
|
||||
let scale = output.current_scale().fractional_scale();
|
||||
let size: smithay::utils::Size<i32, smithay::utils::Physical> =
|
||||
geometry.size.to_physical_precise_round(scale);
|
||||
let size: smithay::utils::Size<i32, smithay::utils::Physical> = geometry.size.to_physical_precise_round(scale);
|
||||
let bytes = usize::try_from(size.w)
|
||||
.ok()?
|
||||
.checked_mul(usize::try_from(size.h).ok()?)?
|
||||
@@ -247,8 +221,7 @@ impl Ferese {
|
||||
}
|
||||
let (x, y) = self.inverse_presented_window_point(id, location.x, location.y)?;
|
||||
let surface_point = smithay::utils::Point::from((x, y)) + geometry.loc.to_f64();
|
||||
let (surface, _) = window
|
||||
.surface_under(surface_point, smithay::desktop::WindowSurfaceType::ALL)?;
|
||||
let (surface, _) = window.surface_under(surface_point, smithay::desktop::WindowSurfaceType::ALL)?;
|
||||
if pointer.current_focus().as_ref() != Some(&surface) {
|
||||
return None;
|
||||
}
|
||||
@@ -298,18 +271,10 @@ impl Ferese {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn copy_snapshot(
|
||||
state: &Ferese,
|
||||
resource: &ZwlrScreencopyFrameV1,
|
||||
data: &FrameData,
|
||||
buffer: &WlBuffer,
|
||||
) {
|
||||
pub(super) fn copy_snapshot(state: &Ferese, resource: &ZwlrScreencopyFrameV1, data: &FrameData, buffer: &WlBuffer) {
|
||||
let mut used = data.used.lock().unwrap();
|
||||
if *used {
|
||||
resource.post_error(
|
||||
frame::Error::AlreadyUsed,
|
||||
"window capture frame has already been used",
|
||||
);
|
||||
resource.post_error(frame::Error::AlreadyUsed, "window capture frame has already been used");
|
||||
return;
|
||||
}
|
||||
*used = true;
|
||||
@@ -342,11 +307,7 @@ pub(super) fn copy_snapshot(
|
||||
// SAFETY: Smithay owns the writable mapping, validated format/stride and
|
||||
// checked offset cover the complete snapshot; the source is separately owned.
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(
|
||||
snapshot.pixels.pixels.as_ptr(),
|
||||
destination.add(offset),
|
||||
bytes,
|
||||
);
|
||||
std::ptr::copy_nonoverlapping(snapshot.pixels.pixels.as_ptr(), destination.add(offset), bytes);
|
||||
}
|
||||
true
|
||||
})
|
||||
|
||||
@@ -1,35 +1,23 @@
|
||||
use smithay::{
|
||||
desktop::{
|
||||
PopupKeyboardGrab, PopupKind, PopupManager, PopupPointerGrab, Space, Window,
|
||||
WindowSurfaceType, find_popup_root_surface, get_popup_toplevel_coords,
|
||||
},
|
||||
input::{
|
||||
Seat,
|
||||
pointer::{Focus, GrabStartData},
|
||||
},
|
||||
reexports::wayland_protocols::xdg::{
|
||||
decoration::zv1::server::zxdg_toplevel_decoration_v1::Mode,
|
||||
shell::server::xdg_toplevel::State as ToplevelState,
|
||||
},
|
||||
reexports::wayland_server::{
|
||||
Resource,
|
||||
protocol::{wl_seat, wl_surface::WlSurface},
|
||||
},
|
||||
utils::Serial,
|
||||
wayland::{
|
||||
compositor::with_states,
|
||||
shell::xdg::decoration::XdgDecorationHandler,
|
||||
shell::xdg::{
|
||||
PopupSurface, PositionerState, ToplevelSurface, XdgShellHandler, XdgShellState,
|
||||
XdgToplevelSurfaceData,
|
||||
},
|
||||
},
|
||||
use smithay::desktop::{
|
||||
PopupKeyboardGrab, PopupKind, PopupManager, PopupPointerGrab, Space, Window, WindowSurfaceType,
|
||||
find_popup_root_surface, get_popup_toplevel_coords,
|
||||
};
|
||||
use smithay::input::Seat;
|
||||
use smithay::input::pointer::{Focus, GrabStartData};
|
||||
use smithay::reexports::wayland_protocols::xdg::decoration::zv1::server::zxdg_toplevel_decoration_v1::Mode;
|
||||
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::State as ToplevelState;
|
||||
use smithay::reexports::wayland_server::Resource;
|
||||
use smithay::reexports::wayland_server::protocol::wl_seat;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::utils::{Logical, Point, Rectangle, Serial};
|
||||
use smithay::wayland::compositor::with_states;
|
||||
use smithay::wayland::shell::xdg::decoration::XdgDecorationHandler;
|
||||
use smithay::wayland::shell::xdg::{
|
||||
PopupSurface, PositionerState, ToplevelSurface, XdgShellHandler, XdgShellState, XdgToplevelSurfaceData,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
Ferese,
|
||||
grabs::{MoveSurfaceGrab, ResizeEdge, ResizeSurfaceGrab, handle_resize_commit},
|
||||
};
|
||||
use crate::Ferese;
|
||||
use crate::grabs::{MoveSurfaceGrab, ResizeEdge, ResizeSurfaceGrab, handle_resize_commit};
|
||||
|
||||
impl XdgShellHandler for Ferese {
|
||||
fn xdg_shell_state(&mut self) -> &mut XdgShellState {
|
||||
@@ -93,12 +81,7 @@ impl XdgShellHandler for Ferese {
|
||||
}
|
||||
}
|
||||
|
||||
fn reposition_request(
|
||||
&mut self,
|
||||
surface: PopupSurface,
|
||||
positioner: PositionerState,
|
||||
token: u32,
|
||||
) {
|
||||
fn reposition_request(&mut self, surface: PopupSurface, positioner: PositionerState, token: u32) {
|
||||
surface.with_pending_state(|state| {
|
||||
state.geometry = positioner.get_geometry();
|
||||
state.positioner = positioner;
|
||||
@@ -273,12 +256,7 @@ impl XdgShellHandler for Ferese {
|
||||
};
|
||||
|
||||
if let Some(pointer) = seat.get_pointer() {
|
||||
pointer.set_grab(
|
||||
self,
|
||||
PopupPointerGrab::new(&popup_grab),
|
||||
serial,
|
||||
Focus::Keep,
|
||||
);
|
||||
pointer.set_grab(self, PopupPointerGrab::new(&popup_grab), serial, Focus::Keep);
|
||||
}
|
||||
if let Some(keyboard) = seat.get_keyboard() {
|
||||
keyboard.set_grab(self, PopupKeyboardGrab::new(&popup_grab), serial);
|
||||
@@ -290,21 +268,14 @@ impl XdgShellHandler for Ferese {
|
||||
}
|
||||
}
|
||||
|
||||
fn check_grab(
|
||||
seat: &Seat<Ferese>,
|
||||
surface: &WlSurface,
|
||||
serial: Serial,
|
||||
) -> Option<GrabStartData<Ferese>> {
|
||||
fn check_grab(seat: &Seat<Ferese>, surface: &WlSurface, serial: Serial) -> Option<GrabStartData<Ferese>> {
|
||||
let pointer = seat.get_pointer()?;
|
||||
if !pointer.has_grab(serial) {
|
||||
return None;
|
||||
}
|
||||
let start_data = pointer.grab_start_data()?;
|
||||
let (focus, _) = start_data.focus.as_ref()?;
|
||||
focus
|
||||
.id()
|
||||
.same_client_as(&surface.id())
|
||||
.then_some(start_data)
|
||||
focus.id().same_client_as(&surface.id()).then_some(start_data)
|
||||
}
|
||||
|
||||
impl XdgDecorationHandler for Ferese {
|
||||
@@ -381,9 +352,7 @@ pub fn handle_commit(popups: &mut PopupManager, space: &mut Space<Window>, surfa
|
||||
if let Some(PopupKind::Xdg(popup)) = popups.find_popup(surface)
|
||||
&& !popup.is_initial_configure_sent()
|
||||
{
|
||||
popup
|
||||
.send_configure()
|
||||
.expect("initial popup configure is valid");
|
||||
popup.send_configure().expect("initial popup configure is valid");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -444,40 +413,82 @@ impl Ferese {
|
||||
let Ok(root) = find_popup_root_surface(&PopupKind::Xdg(popup.clone())) else {
|
||||
return;
|
||||
};
|
||||
let window = self.space.elements().find(|window| {
|
||||
window
|
||||
.toplevel()
|
||||
.is_some_and(|toplevel| toplevel.wl_surface() == &root)
|
||||
});
|
||||
let window = self
|
||||
.space
|
||||
.elements()
|
||||
.find(|window| window.toplevel().is_some_and(|toplevel| toplevel.wl_surface() == &root));
|
||||
let geometry = (|| {
|
||||
if let Some(window) = window {
|
||||
let output = self.space.outputs().next()?;
|
||||
let output_geometry = self.space.output_geometry(output)?;
|
||||
let window_geometry = self.space.element_geometry(window)?;
|
||||
|
||||
Some((output_geometry, window_geometry.loc))
|
||||
} else {
|
||||
let layer = self
|
||||
let id = *self.window_ids.get(window)?;
|
||||
let output = self
|
||||
.space
|
||||
.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL)?;
|
||||
.outputs()
|
||||
.find(|output| self.window_belongs_to_output(id, output))?;
|
||||
let output_geometry = self.space.output_geometry(output)?;
|
||||
let presented = self.presented_window_rect(id)?;
|
||||
let location = (presented.x.round() as i32, presented.y.round() as i32).into();
|
||||
|
||||
Some((output_geometry, location))
|
||||
} else {
|
||||
let layer = self.space.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL)?;
|
||||
|
||||
self.space.outputs().find_map(|output| {
|
||||
let output_geometry = self.space.output_geometry(output)?;
|
||||
let layer_geometry =
|
||||
smithay::desktop::layer_map_for_output(output).layer_geometry(&layer)?;
|
||||
let layer_geometry = smithay::desktop::layer_map_for_output(output).layer_geometry(&layer)?;
|
||||
|
||||
Some((output_geometry, output_geometry.loc + layer_geometry.loc))
|
||||
})
|
||||
}
|
||||
})();
|
||||
let Some((mut target, root_location)) = geometry else {
|
||||
let Some((target, root_location)) = geometry else {
|
||||
return;
|
||||
};
|
||||
|
||||
target.loc -= get_popup_toplevel_coords(&PopupKind::Xdg(popup.clone()));
|
||||
target.loc -= root_location;
|
||||
let target = popup_constraint_target(
|
||||
target,
|
||||
root_location,
|
||||
get_popup_toplevel_coords(&PopupKind::Xdg(popup.clone())),
|
||||
);
|
||||
popup.with_pending_state(|state| {
|
||||
state.geometry = state.positioner.get_unconstrained_geometry(target);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn popup_constraint_target(
|
||||
mut output: Rectangle<i32, Logical>,
|
||||
root_location: Point<i32, Logical>,
|
||||
parent_offset: Point<i32, Logical>,
|
||||
) -> Rectangle<i32, Logical> {
|
||||
output.loc -= root_location + parent_offset;
|
||||
output
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_positioner;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn popup_uses_presented_parent_coordinates_on_an_offset_monitor() {
|
||||
let output = Rectangle::new((1920, -200).into(), (1920, 1080).into());
|
||||
let root = Point::from((2200, -100));
|
||||
let parent = Point::from((40, 30));
|
||||
let target = popup_constraint_target(output, root, parent);
|
||||
assert_eq!(target.loc, Point::from((-320, -130)));
|
||||
let positioner = PositionerState {
|
||||
rect_size: (320, 180).into(),
|
||||
anchor_rect: Rectangle::new((1850, 900).into(), (1, 1).into()),
|
||||
anchor_edges: xdg_positioner::Anchor::BottomRight,
|
||||
gravity: xdg_positioner::Gravity::BottomRight,
|
||||
constraint_adjustment: xdg_positioner::ConstraintAdjustment::SlideX
|
||||
| xdg_positioner::ConstraintAdjustment::SlideY,
|
||||
..Default::default()
|
||||
};
|
||||
let popup = positioner.get_unconstrained_geometry(target);
|
||||
assert_eq!(popup.loc, Point::from((1280, 770)));
|
||||
let global = Rectangle::new(root + parent + popup.loc, popup.size);
|
||||
assert_eq!(global.intersection(output), Some(global));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user