Format Rust sources at 100 columns
This commit is contained in:
1 parent
4661f595f8
commit
610478f8c7
336 files changed
+23497
-7892
No files matched your search
@@ -40,7 +40,9 @@ impl<C: PartialEq> InputHistory<C> {
|
||||
|
||||
fn validate(&self, serial: Serial, client: &C, now: Instant) -> Option<Instant> {
|
||||
self.events.iter().rev().find_map(|(event, owner, time)| {
|
||||
(*event == serial && owner == client && now.saturating_duration_since(*time) <= TOKEN_MAX_AGE)
|
||||
(*event == serial
|
||||
&& owner == client
|
||||
&& now.saturating_duration_since(*time) <= TOKEN_MAX_AGE)
|
||||
.then_some(*time)
|
||||
})
|
||||
}
|
||||
@@ -55,7 +57,8 @@ impl Ferese {
|
||||
}
|
||||
|
||||
if let Some(client) = surface.and_then(|surface| surface.client()) {
|
||||
self.activation_inputs.record(serial, client.id(), Instant::now());
|
||||
self.activation_inputs
|
||||
.record(serial, client.id(), Instant::now());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -86,14 +89,23 @@ impl XdgActivationHandler for Ferese {
|
||||
return false;
|
||||
}
|
||||
|
||||
let Some(time) = self.activation_inputs.validate(*serial, client, Instant::now()) else {
|
||||
let Some(time) = self
|
||||
.activation_inputs
|
||||
.validate(*serial, client, Instant::now())
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
data.user_data.insert_if_missing(|| ApprovedInteraction(time));
|
||||
data.user_data
|
||||
.insert_if_missing(|| ApprovedInteraction(time));
|
||||
true
|
||||
}
|
||||
|
||||
fn request_activation(&mut self, token: XdgActivationToken, data: XdgActivationTokenData, surface: WlSurface) {
|
||||
fn request_activation(
|
||||
&mut self,
|
||||
token: XdgActivationToken,
|
||||
data: XdgActivationTokenData,
|
||||
surface: WlSurface,
|
||||
) {
|
||||
self.xdg_activation_state.remove_token(&token);
|
||||
if self.session_lock.active() || self.input_capture.captures(1) {
|
||||
return;
|
||||
@@ -152,7 +164,9 @@ mod tests {
|
||||
wire.write_all(&bytes).unwrap();
|
||||
};
|
||||
send(&mut wire, 1, 1, &[2]); // wl_display.get_registry
|
||||
events.dispatch(Duration::from_millis(1), &mut state).unwrap();
|
||||
events
|
||||
.dispatch(Duration::from_millis(1), &mut state)
|
||||
.unwrap();
|
||||
state.display_handle.flush_clients().unwrap();
|
||||
wire.set_nonblocking(true).unwrap();
|
||||
let mut globals = Vec::new();
|
||||
@@ -162,10 +176,16 @@ mod tests {
|
||||
let mut seat_name = None;
|
||||
while offset < globals.len() {
|
||||
let header = u32::from_ne_bytes(globals[offset + 4..offset + 8].try_into().unwrap());
|
||||
if u32::from_ne_bytes(globals[offset..offset + 4].try_into().unwrap()) == 2 && header & 0xffff == 0 {
|
||||
let length = u32::from_ne_bytes(globals[offset + 12..offset + 16].try_into().unwrap()) as usize;
|
||||
if u32::from_ne_bytes(globals[offset..offset + 4].try_into().unwrap()) == 2
|
||||
&& header & 0xffff == 0
|
||||
{
|
||||
let length =
|
||||
u32::from_ne_bytes(globals[offset + 12..offset + 16].try_into().unwrap())
|
||||
as usize;
|
||||
if &globals[offset + 16..offset + 16 + length] == b"wl_seat\0" {
|
||||
seat_name = Some(u32::from_ne_bytes(globals[offset + 8..offset + 12].try_into().unwrap()));
|
||||
seat_name = Some(u32::from_ne_bytes(
|
||||
globals[offset + 8..offset + 12].try_into().unwrap(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,7 +200,9 @@ mod tests {
|
||||
);
|
||||
args.extend([7, 3]);
|
||||
send(&mut wire, 2, 0, &args);
|
||||
events.dispatch(Duration::from_millis(1), &mut state).unwrap();
|
||||
events
|
||||
.dispatch(Duration::from_millis(1), &mut state)
|
||||
.unwrap();
|
||||
let seat: WlSeat = state
|
||||
.seat
|
||||
.client_seats(&launcher)
|
||||
@@ -208,7 +230,9 @@ mod tests {
|
||||
!state.token_created(token.clone(), data()),
|
||||
"fresh token renewed stale input"
|
||||
);
|
||||
state.activation_inputs.record(serial, launcher.id(), Instant::now());
|
||||
state
|
||||
.activation_inputs
|
||||
.record(serial, launcher.id(), Instant::now());
|
||||
let approved = data();
|
||||
assert!(state.token_created(token.clone(), approved.clone()));
|
||||
assert!(approved.user_data.get::<ApprovedInteraction>().is_some());
|
||||
@@ -227,7 +251,15 @@ mod tests {
|
||||
// The proof is attached to the token, not the recipient identity. This
|
||||
// is what lets a validated launcher hand it to a different application.
|
||||
assert_ne!(approved.client_id, Some(recipient.id()));
|
||||
assert!(approved.user_data.get::<ApprovedInteraction>().unwrap().0.elapsed() < TOKEN_MAX_AGE);
|
||||
assert!(
|
||||
approved
|
||||
.user_data
|
||||
.get::<ApprovedInteraction>()
|
||||
.unwrap()
|
||||
.0
|
||||
.elapsed()
|
||||
< TOKEN_MAX_AGE
|
||||
);
|
||||
|
||||
// A valid handoff must not focus a recipient before its first buffer.
|
||||
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_wm_base::XdgWmBase;
|
||||
@@ -241,12 +273,18 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
let shell = recipient
|
||||
.create_resource::<XdgWmBase, XdgWmBaseUserData, Ferese>(&state.display_handle, 6, Default::default())
|
||||
.create_resource::<XdgWmBase, XdgWmBaseUserData, Ferese>(
|
||||
&state.display_handle,
|
||||
6,
|
||||
Default::default(),
|
||||
)
|
||||
.unwrap();
|
||||
send(&mut recipient_wire, compositor.id().protocol_id(), 0, &[2]);
|
||||
send(&mut recipient_wire, shell.id().protocol_id(), 2, &[3, 2]);
|
||||
send(&mut recipient_wire, 3, 1, &[4]);
|
||||
events.dispatch(Duration::from_millis(1), &mut state).unwrap();
|
||||
events
|
||||
.dispatch(Duration::from_millis(1), &mut state)
|
||||
.unwrap();
|
||||
let target = state
|
||||
.space
|
||||
.elements()
|
||||
@@ -272,7 +310,11 @@ mod tests {
|
||||
assert_eq!(history.validate(Serial::from(13), &"launcher", now), None);
|
||||
assert_eq!(history.validate(serial, &"other-client", now), None);
|
||||
assert_eq!(
|
||||
history.validate(serial, &"launcher", now + TOKEN_MAX_AGE + Duration::from_millis(1)),
|
||||
history.validate(
|
||||
serial,
|
||||
&"launcher",
|
||||
now + TOKEN_MAX_AGE + Duration::from_millis(1)
|
||||
),
|
||||
None
|
||||
);
|
||||
// An approved token retains the interaction time across client handoff.
|
||||
|
||||
@@ -5,7 +5,8 @@ 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, get_role, is_sync_subsurface,
|
||||
CompositorClientState, CompositorHandler, CompositorState, get_parent, get_role,
|
||||
is_sync_subsurface,
|
||||
};
|
||||
use smithay::wayland::dmabuf::{DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier};
|
||||
use smithay::wayland::shm::{ShmHandler, ShmState};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::Ferese;
|
||||
use smithay::reexports::wayland_server::{
|
||||
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, backend::ClientId, protocol as wl,
|
||||
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, backend::ClientId,
|
||||
protocol as wl,
|
||||
};
|
||||
use smithay::reexports::{
|
||||
wayland_protocols as protocols, wayland_protocols_misc as misc, wayland_protocols_wlr as wlr,
|
||||
|
||||
@@ -5,7 +5,9 @@ use smithay::reexports::wayland_protocols_wlr::gamma_control::v1::server::{
|
||||
zwlr_gamma_control_manager_v1::{self, ZwlrGammaControlManagerV1},
|
||||
zwlr_gamma_control_v1::{self, ZwlrGammaControlV1},
|
||||
};
|
||||
use smithay::reexports::wayland_server::{Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource};
|
||||
use smithay::reexports::wayland_server::{
|
||||
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
|
||||
};
|
||||
|
||||
use crate::{Ferese, backends::direct::gamma, gamma::Ramp};
|
||||
|
||||
|
||||
@@ -7,7 +7,10 @@ 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_surface(self, surface.wl_surface());
|
||||
@@ -34,7 +37,10 @@ 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,4 +1,6 @@
|
||||
use smithay::desktop::{LayerSurface as DesktopLayerSurface, PopupKind, WindowSurfaceType, layer_map_for_output};
|
||||
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;
|
||||
@@ -14,7 +16,13 @@ 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,
|
||||
) {
|
||||
// A wl_output resource can outlive its registered desktop output.
|
||||
// Explicit requests retain their identity; only unspecified requests
|
||||
// may use the focused/default output policy.
|
||||
@@ -82,7 +90,11 @@ impl WlrLayerShellHandler for Ferese {
|
||||
let outputs = self
|
||||
.space
|
||||
.outputs()
|
||||
.filter(|output| layer_map_for_output(output).layer_geometry(&layer).is_some())
|
||||
.filter(|output| {
|
||||
layer_map_for_output(output)
|
||||
.layer_geometry(&layer)
|
||||
.is_some()
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@@ -134,13 +146,20 @@ impl LayoutState {
|
||||
}
|
||||
|
||||
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()
|
||||
.filter(|output| layer_map_for_output(output).layer_geometry(&layer).is_some())
|
||||
.filter(|output| {
|
||||
layer_map_for_output(output)
|
||||
.layer_geometry(&layer)
|
||||
.is_some()
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@@ -154,9 +173,9 @@ pub fn handle_commit(state: &mut Ferese, surface: &WlSurface) {
|
||||
}
|
||||
let policy = layer.cached_state();
|
||||
let current = LayoutState::current(&layer);
|
||||
let previous = layer
|
||||
.user_data()
|
||||
.get_or_insert_threadsafe(|| std::sync::Mutex::new(None::<(LayoutState, KeyboardInteractivity)>));
|
||||
let previous = layer.user_data().get_or_insert_threadsafe(|| {
|
||||
std::sync::Mutex::new(None::<(LayoutState, KeyboardInteractivity)>)
|
||||
});
|
||||
let (changed, requested_focus) = {
|
||||
let mut previous = previous.lock().unwrap();
|
||||
let changed = previous.as_ref().map(|(layout, _)| layout) != Some(¤t);
|
||||
@@ -191,7 +210,10 @@ pub fn handle_commit(state: &mut Ferese, surface: &WlSurface) {
|
||||
};
|
||||
state.reconcile_layer_keyboard_focus();
|
||||
if opening_interaction
|
||||
&& !state.seat.get_keyboard().is_some_and(|keyboard| keyboard.is_grabbed())
|
||||
&& !state
|
||||
.seat
|
||||
.get_keyboard()
|
||||
.is_some_and(|keyboard| keyboard.is_grabbed())
|
||||
&& !state.session_lock.active()
|
||||
&& !state.input_capture.captures(1)
|
||||
&& state.layer_keyboard_eligible(&layer)
|
||||
@@ -199,11 +221,15 @@ pub fn handle_commit(state: &mut Ferese, surface: &WlSurface) {
|
||||
&& matches!(policy.layer, Layer::Top | Layer::Overlay)
|
||||
&& !layer_has_keyboard_focus(state, &layer)
|
||||
{
|
||||
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(),
|
||||
);
|
||||
}
|
||||
|
||||
if let Err(error) = state.display_handle.flush_clients() {
|
||||
@@ -220,9 +246,10 @@ impl Ferese {
|
||||
/// Use the renderer state updated by on_commit_buffer_handler, as for windows.
|
||||
fn layer_mapped_and_visible(&self, layer: &DesktopLayerSurface) -> bool {
|
||||
layer.layer_surface().alive()
|
||||
&& smithay::backend::renderer::utils::with_renderer_surface_state(layer.wl_surface(), |state| {
|
||||
state.buffer().is_some()
|
||||
})
|
||||
&& smithay::backend::renderer::utils::with_renderer_surface_state(
|
||||
layer.wl_surface(),
|
||||
|state| state.buffer().is_some(),
|
||||
)
|
||||
.unwrap_or(false)
|
||||
&& self.space.outputs().any(|output| {
|
||||
self.layer_visible_on_output(output, layer.layer())
|
||||
@@ -239,8 +266,9 @@ impl Ferese {
|
||||
.find_popup(&surface)
|
||||
.and_then(|popup| smithay::desktop::find_popup_root_surface(&popup).ok())
|
||||
.unwrap_or(surface);
|
||||
(smithay::wayland::compositor::get_role(&root) == Some(smithay::wayland::shell::wlr_layer::LAYER_SURFACE_ROLE))
|
||||
.then_some(root)
|
||||
(smithay::wayland::compositor::get_role(&root)
|
||||
== Some(smithay::wayland::shell::wlr_layer::LAYER_SURFACE_ROLE))
|
||||
.then_some(root)
|
||||
}
|
||||
|
||||
pub(crate) fn keyboard_focused_layer(&self) -> Option<DesktopLayerSurface> {
|
||||
@@ -255,7 +283,8 @@ impl Ferese {
|
||||
for priority in [Layer::Overlay, Layer::Top] {
|
||||
if let Some(layer) = focused.as_ref().filter(|layer| {
|
||||
layer.layer() == priority
|
||||
&& layer.cached_state().keyboard_interactivity == KeyboardInteractivity::Exclusive
|
||||
&& layer.cached_state().keyboard_interactivity
|
||||
== KeyboardInteractivity::Exclusive
|
||||
&& self.layer_keyboard_eligible(layer)
|
||||
}) {
|
||||
return Some(layer.clone());
|
||||
@@ -313,7 +342,9 @@ impl Ferese {
|
||||
}
|
||||
|
||||
fn layer_has_keyboard_focus(state: &Ferese, layer: &DesktopLayerSurface) -> bool {
|
||||
state.keyboard_focused_layer().is_some_and(|focused| focused == *layer)
|
||||
state
|
||||
.keyboard_focused_layer()
|
||||
.is_some_and(|focused| focused == *layer)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -349,7 +380,10 @@ mod tests {
|
||||
original,
|
||||
LayoutState::from_policy(policy, Rectangle::from_size((800, 33).into()))
|
||||
);
|
||||
assert_ne!(original, LayoutState::from_policy(policy, Rectangle::default()));
|
||||
assert_ne!(
|
||||
original,
|
||||
LayoutState::from_policy(policy, Rectangle::default())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -151,7 +151,13 @@ impl Client {
|
||||
) -> Vec<Vec<u32>> {
|
||||
if output == 0 {
|
||||
// Reserve the output slot with a spare surface for unspecified requests.
|
||||
request(&mut self.wire, self.compositor.id().protocol_id(), 0, &[5], None);
|
||||
request(
|
||||
&mut self.wire,
|
||||
self.compositor.id().protocol_id(),
|
||||
0,
|
||||
&[5],
|
||||
None,
|
||||
);
|
||||
}
|
||||
request(
|
||||
&mut self.wire,
|
||||
@@ -199,8 +205,9 @@ fn assert_closed_without_configure(messages: &[Vec<u32>]) {
|
||||
|
||||
#[test]
|
||||
fn zero_outputs_close_new_layer_roles() {
|
||||
if !crate::startup_tests::private_runtime("handlers::layer_shell::output_tests::zero_outputs_close_new_layer_roles")
|
||||
{
|
||||
if !crate::startup_tests::private_runtime(
|
||||
"handlers::layer_shell::output_tests::zero_outputs_close_new_layer_roles",
|
||||
) {
|
||||
return;
|
||||
}
|
||||
let (mut state, mut events) = fixture();
|
||||
@@ -259,8 +266,9 @@ fn unspecified_output_uses_focused_registered_output() {
|
||||
|
||||
#[test]
|
||||
fn valid_explicit_output_is_preserved() {
|
||||
if !crate::startup_tests::private_runtime("handlers::layer_shell::output_tests::valid_explicit_output_is_preserved")
|
||||
{
|
||||
if !crate::startup_tests::private_runtime(
|
||||
"handlers::layer_shell::output_tests::valid_explicit_output_is_preserved",
|
||||
) {
|
||||
return;
|
||||
}
|
||||
let (mut state, mut events) = fixture();
|
||||
|
||||
@@ -28,7 +28,9 @@ use smithay::wayland::keyboard_shortcuts_inhibit::{
|
||||
KeyboardShortcutsInhibitorSeat,
|
||||
};
|
||||
use smithay::wayland::output::OutputHandler;
|
||||
use smithay::wayland::pointer_constraints::{PointerConstraint, PointerConstraintsHandler, with_pointer_constraint};
|
||||
use smithay::wayland::pointer_constraints::{
|
||||
PointerConstraint, PointerConstraintsHandler, with_pointer_constraint,
|
||||
};
|
||||
use smithay::wayland::selection::SelectionHandler;
|
||||
use smithay::wayland::selection::data_device::{
|
||||
DataDeviceHandler, DataDeviceState, WaylandDndGrabHandler, set_data_device_focus,
|
||||
@@ -54,12 +56,17 @@ impl SeatHandler for Ferese {
|
||||
if let CursorImageStatus::Named(icon) = &image
|
||||
&& !self.named_cursors.contains_key(icon)
|
||||
{
|
||||
self.cursor_loader.request(*icon, self.cursor_animation.buffer_scale);
|
||||
self.cursor_loader
|
||||
.request(*icon, self.cursor_animation.buffer_scale);
|
||||
}
|
||||
self.cursor_animation.reset |= match (&self.cursor_status, &image) {
|
||||
(CursorImageStatus::Named(previous), CursorImageStatus::Named(next)) => previous != next,
|
||||
(CursorImageStatus::Named(previous), CursorImageStatus::Named(next)) => {
|
||||
previous != next
|
||||
}
|
||||
(CursorImageStatus::Hidden, CursorImageStatus::Hidden) => false,
|
||||
(CursorImageStatus::Surface(previous), CursorImageStatus::Surface(next)) => previous != next,
|
||||
(CursorImageStatus::Surface(previous), CursorImageStatus::Surface(next)) => {
|
||||
previous != next
|
||||
}
|
||||
_ => true,
|
||||
};
|
||||
self.cursor_status = image;
|
||||
@@ -74,7 +81,10 @@ impl SeatHandler for Ferese {
|
||||
self.pending_layer_focus = None;
|
||||
// Smithay has already sent enter. last_enter uses a separate mutex;
|
||||
// querying current_focus here would reenter the held keyboard lock.
|
||||
if let Some(serial) = seat.get_keyboard().and_then(|keyboard| keyboard.last_enter()) {
|
||||
if let Some(serial) = seat
|
||||
.get_keyboard()
|
||||
.and_then(|keyboard| keyboard.last_enter())
|
||||
{
|
||||
self.record_activation_input(serial, focused.cloned());
|
||||
}
|
||||
|
||||
@@ -86,7 +96,9 @@ 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);
|
||||
}
|
||||
@@ -136,7 +148,10 @@ 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();
|
||||
@@ -216,7 +231,9 @@ impl Ferese {
|
||||
let output = output
|
||||
.or_else(|| self.focused_output())
|
||||
.or_else(|| self.space.outputs().next());
|
||||
with_states(surface, |states| set_surface_preferences(surface, states, output));
|
||||
with_states(surface, |states| {
|
||||
set_surface_preferences(surface, states, output)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,9 +243,18 @@ fn set_surface_preferences(
|
||||
output: Option<&smithay::output::Output>,
|
||||
) {
|
||||
let scale = output.map_or(1.0, |output| output.current_scale().fractional_scale());
|
||||
let transform = output.map_or(smithay::utils::Transform::Normal, |output| output.current_transform());
|
||||
smithay::wayland::compositor::send_surface_state(surface, states, scale.ceil() as i32, transform);
|
||||
with_fractional_scale(states, |surface_scale| surface_scale.set_preferred_scale(scale));
|
||||
let transform = output.map_or(smithay::utils::Transform::Normal, |output| {
|
||||
output.current_transform()
|
||||
});
|
||||
smithay::wayland::compositor::send_surface_state(
|
||||
surface,
|
||||
states,
|
||||
scale.ceil() as i32,
|
||||
transform,
|
||||
);
|
||||
with_fractional_scale(states, |surface_scale| {
|
||||
surface_scale.set_preferred_scale(scale)
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn set_surface_tree_output(surface: &WlSurface, output: &smithay::output::Output) {
|
||||
@@ -337,13 +363,17 @@ impl PrimarySelectionHandler for Ferese {
|
||||
}
|
||||
|
||||
impl smithay::wayland::selection::wlr_data_control::DataControlHandler for Ferese {
|
||||
fn data_control_state(&mut self) -> &mut smithay::wayland::selection::wlr_data_control::DataControlState {
|
||||
fn data_control_state(
|
||||
&mut self,
|
||||
) -> &mut smithay::wayland::selection::wlr_data_control::DataControlState {
|
||||
&mut self.wlr_data_control_state
|
||||
}
|
||||
}
|
||||
|
||||
impl smithay::wayland::selection::ext_data_control::DataControlHandler for Ferese {
|
||||
fn data_control_state(&mut self) -> &mut smithay::wayland::selection::ext_data_control::DataControlState {
|
||||
fn data_control_state(
|
||||
&mut self,
|
||||
) -> &mut smithay::wayland::selection::ext_data_control::DataControlState {
|
||||
&mut self.ext_data_control_state
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,9 @@ use smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::{
|
||||
};
|
||||
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::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;
|
||||
|
||||
@@ -157,7 +159,8 @@ 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 {
|
||||
@@ -321,7 +324,9 @@ 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;
|
||||
}
|
||||
@@ -332,7 +337,8 @@ 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();
|
||||
}
|
||||
}
|
||||
@@ -342,15 +348,18 @@ 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)
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn fail_screencopies(&mut self, output: &Output, overlay_cursor: bool) {
|
||||
let mut remaining = Vec::new();
|
||||
for capture in std::mem::take(&mut self.pending_screencopies) {
|
||||
if capture.output == *output && cursor_overlay_matches(capture.overlay_cursor, overlay_cursor) {
|
||||
if capture.output == *output
|
||||
&& cursor_overlay_matches(capture.overlay_cursor, overlay_cursor)
|
||||
{
|
||||
capture.sink.fail();
|
||||
} else {
|
||||
remaining.push(capture);
|
||||
@@ -422,7 +431,10 @@ 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();
|
||||
@@ -458,7 +470,9 @@ pub(crate) fn capture_region_for_geometry(
|
||||
}
|
||||
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;
|
||||
@@ -509,7 +523,9 @@ 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(());
|
||||
@@ -567,7 +583,11 @@ 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(())
|
||||
}
|
||||
@@ -596,7 +616,8 @@ 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()));
|
||||
}
|
||||
@@ -606,7 +627,9 @@ 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]
|
||||
|
||||
@@ -40,7 +40,9 @@ 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;
|
||||
}
|
||||
@@ -83,7 +85,11 @@ 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;
|
||||
@@ -95,8 +101,13 @@ fn scale_and_convert(frame: &OutputFrame, target_width: usize, target_height: us
|
||||
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)?;
|
||||
@@ -147,7 +158,13 @@ fn scale_and_convert(frame: &OutputFrame, target_width: usize, target_height: us
|
||||
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),
|
||||
@@ -173,7 +190,10 @@ 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());
|
||||
}
|
||||
@@ -200,15 +220,22 @@ 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)
|
||||
@@ -269,7 +296,12 @@ 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 {
|
||||
@@ -279,9 +311,16 @@ 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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -402,7 +441,8 @@ impl Budget {
|
||||
fn reserve(self: &Arc<Self>, bytes: usize) -> Result<BudgetPermit, String> {
|
||||
self.used
|
||||
.fetch_update(Ordering::AcqRel, Ordering::Acquire, |used| {
|
||||
used.checked_add(bytes).filter(|total| *total <= MAX_PENDING_BYTES)
|
||||
used.checked_add(bytes)
|
||||
.filter(|total| *total <= MAX_PENDING_BYTES)
|
||||
})
|
||||
.map_err(|_| "Screenshot memory budget is exhausted".to_string())?;
|
||||
Ok(BudgetPermit {
|
||||
@@ -492,7 +532,10 @@ 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: now + REQUEST_TIMEOUT,
|
||||
permit: Some(permit),
|
||||
@@ -503,7 +546,9 @@ impl Coordinator {
|
||||
}
|
||||
|
||||
pub(crate) fn permit(&self, request: u64) -> Option<BudgetPermit> {
|
||||
self.requests.get(&request).and_then(|entry| entry.permit.clone())
|
||||
self.requests
|
||||
.get(&request)
|
||||
.and_then(|entry| entry.permit.clone())
|
||||
}
|
||||
|
||||
// Used by the completion tests to assert that a request is still tracked.
|
||||
@@ -519,7 +564,12 @@ 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;
|
||||
};
|
||||
@@ -572,7 +622,10 @@ impl Coordinator {
|
||||
Action::Encode {
|
||||
request,
|
||||
frames,
|
||||
permit: entry.permit.take().expect("collecting request owns its reservation"),
|
||||
permit: entry
|
||||
.permit
|
||||
.take()
|
||||
.expect("collecting request owns its reservation"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -613,7 +666,10 @@ 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
|
||||
}
|
||||
@@ -728,7 +784,10 @@ 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());
|
||||
}
|
||||
@@ -739,7 +798,8 @@ pub(crate) fn plan(geometry: &Geometry, outputs: &[OutputLayout]) -> Result<Vec<
|
||||
{
|
||||
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 => {
|
||||
@@ -751,8 +811,10 @@ pub(crate) fn plan(geometry: &Geometry, outputs: &[OutputLayout]) -> Result<Vec<
|
||||
};
|
||||
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),
|
||||
@@ -764,7 +826,12 @@ pub(crate) fn plan(geometry: &Geometry, outputs: &[OutputLayout]) -> Result<Vec<
|
||||
}
|
||||
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(),
|
||||
},
|
||||
@@ -792,7 +859,11 @@ pub(crate) fn plan(geometry: &Geometry, outputs: &[OutputLayout]) -> Result<Vec<
|
||||
}
|
||||
// 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
|
||||
@@ -839,19 +910,34 @@ 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[1].index, parts[1].logical.loc.x, parts[1].logical.size.w),
|
||||
(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
|
||||
),
|
||||
(1, 1920, 80)
|
||||
);
|
||||
// The clipped widths must add back up to the request.
|
||||
@@ -884,8 +970,14 @@ 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);
|
||||
@@ -895,7 +987,10 @@ 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.
|
||||
@@ -906,7 +1001,10 @@ 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");
|
||||
}
|
||||
@@ -916,11 +1014,17 @@ 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]
|
||||
@@ -930,10 +1034,16 @@ 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]
|
||||
@@ -1013,9 +1123,17 @@ mod completion {
|
||||
let (reply, received) = sync_channel(8);
|
||||
let mut coordinator = Coordinator::new();
|
||||
coordinator
|
||||
.admit(1, 42, reply.clone(), (0..parts).map(|_| spec(2, 2)).collect())
|
||||
.admit(
|
||||
1,
|
||||
42,
|
||||
reply.clone(),
|
||||
(0..parts).map(|_| spec(2, 2)).collect(),
|
||||
)
|
||||
.expect("admitted");
|
||||
Self { coordinator, received }
|
||||
Self {
|
||||
coordinator,
|
||||
received,
|
||||
}
|
||||
}
|
||||
|
||||
fn response(&self) -> Option<Response> {
|
||||
@@ -1036,7 +1154,11 @@ mod completion {
|
||||
};
|
||||
|
||||
assert_eq!(frames.len(), 1);
|
||||
assert_eq!(frames[0].pixels.as_ptr(), allocation, "readback pixels were copied");
|
||||
assert_eq!(
|
||||
frames[0].pixels.as_ptr(),
|
||||
allocation,
|
||||
"readback pixels were copied"
|
||||
);
|
||||
assert_eq!(frames[0].pixels, vec![137; 16]);
|
||||
}
|
||||
|
||||
@@ -1049,13 +1171,21 @@ 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,7 +1200,9 @@ 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());
|
||||
|
||||
@@ -1109,14 +1241,26 @@ mod completion {
|
||||
fn admission_enforces_one_budget_across_requests_and_late_readbacks() {
|
||||
let (reply, _received) = sync_channel(8);
|
||||
let mut coordinator = Coordinator::new();
|
||||
let first = coordinator.admit(1, 1, reply.clone(), vec![spec(4096, 8192)]).unwrap();
|
||||
let second = coordinator.admit(1, 2, reply.clone(), vec![spec(4096, 8192)]).unwrap();
|
||||
assert!(coordinator.admit(1, 3, reply.clone(), vec![spec(2, 2)]).is_err());
|
||||
let first = coordinator
|
||||
.admit(1, 1, reply.clone(), vec![spec(4096, 8192)])
|
||||
.unwrap();
|
||||
let second = coordinator
|
||||
.admit(1, 2, reply.clone(), vec![spec(4096, 8192)])
|
||||
.unwrap();
|
||||
assert!(
|
||||
coordinator
|
||||
.admit(1, 3, reply.clone(), vec![spec(2, 2)])
|
||||
.is_err()
|
||||
);
|
||||
|
||||
// A cancelled request can still have a readback retained by a sink or channel.
|
||||
let late_readback = coordinator.permit(first).unwrap();
|
||||
coordinator.reject(first, "cancelled");
|
||||
assert!(coordinator.admit(1, 4, reply.clone(), vec![spec(2, 2)]).is_err());
|
||||
assert!(
|
||||
coordinator
|
||||
.admit(1, 4, reply.clone(), vec![spec(2, 2)])
|
||||
.is_err()
|
||||
);
|
||||
drop(late_readback);
|
||||
assert!(coordinator.admit(1, 5, reply, vec![spec(2, 2)]).is_ok());
|
||||
coordinator.reject(second, "finished");
|
||||
@@ -1127,19 +1271,48 @@ mod completion {
|
||||
let mut harness = Harness::new(1);
|
||||
let id = harness.coordinator.next_id;
|
||||
let bytes = harness.coordinator.budget.used.load(Ordering::Acquire);
|
||||
let Action::Encode { frames, permit, .. } = harness.coordinator.on_part(id, 0, Ok(buffer(2, 2))) else {
|
||||
let Action::Encode { frames, permit, .. } =
|
||||
harness.coordinator.on_part(id, 0, Ok(buffer(2, 2)))
|
||||
else {
|
||||
panic!("ready request should encode");
|
||||
};
|
||||
harness.coordinator.reject(id, "cancelled while encoder owns pixels");
|
||||
assert_eq!(harness.coordinator.budget.used.load(Ordering::Acquire), bytes);
|
||||
assert!(harness.coordinator.budget.reserve(MAX_PENDING_BYTES).is_err());
|
||||
harness
|
||||
.coordinator
|
||||
.reject(id, "cancelled while encoder owns pixels");
|
||||
assert_eq!(
|
||||
harness.coordinator.budget.used.load(Ordering::Acquire),
|
||||
bytes
|
||||
);
|
||||
assert!(
|
||||
harness
|
||||
.coordinator
|
||||
.budget
|
||||
.reserve(MAX_PENDING_BYTES)
|
||||
.is_err()
|
||||
);
|
||||
drop(frames);
|
||||
drop(permit);
|
||||
assert_eq!(harness.coordinator.budget.used.load(Ordering::Acquire), 0);
|
||||
let first = harness.coordinator.budget.reserve(MAX_PENDING_BYTES / 2 + 1).unwrap();
|
||||
assert!(harness.coordinator.budget.reserve(MAX_PENDING_BYTES / 2).is_err());
|
||||
let first = harness
|
||||
.coordinator
|
||||
.budget
|
||||
.reserve(MAX_PENDING_BYTES / 2 + 1)
|
||||
.unwrap();
|
||||
assert!(
|
||||
harness
|
||||
.coordinator
|
||||
.budget
|
||||
.reserve(MAX_PENDING_BYTES / 2)
|
||||
.is_err()
|
||||
);
|
||||
drop(first);
|
||||
assert!(harness.coordinator.budget.reserve(MAX_PENDING_BYTES).is_ok());
|
||||
assert!(
|
||||
harness
|
||||
.coordinator
|
||||
.budget
|
||||
.reserve(MAX_PENDING_BYTES)
|
||||
.is_ok()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1152,7 +1325,10 @@ mod completion {
|
||||
Action::None
|
||||
));
|
||||
let action = harness.coordinator.on_part(id, 1, Ok(buffer(2, 2)));
|
||||
let Action::Encode { request, frames, .. } = action else {
|
||||
let Action::Encode {
|
||||
request, frames, ..
|
||||
} = action
|
||||
else {
|
||||
panic!("expected the request to move to encoding");
|
||||
};
|
||||
assert_eq!(request, id);
|
||||
@@ -1177,7 +1353,9 @@ 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");
|
||||
}
|
||||
@@ -1189,7 +1367,9 @@ mod completion {
|
||||
harness.coordinator.on_part(id, 0, Ok(buffer(2, 2)));
|
||||
assert!(harness.coordinator.is_encoding(id));
|
||||
assert_eq!(
|
||||
harness.coordinator.terminate_all_with_reason("Capture privacy changed"),
|
||||
harness
|
||||
.coordinator
|
||||
.terminate_all_with_reason("Capture privacy changed"),
|
||||
vec![id]
|
||||
);
|
||||
assert!(harness.response().unwrap().error.is_some());
|
||||
@@ -1208,7 +1388,9 @@ 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());
|
||||
@@ -1216,7 +1398,9 @@ 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
|
||||
@@ -1239,7 +1423,9 @@ 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));
|
||||
@@ -1252,13 +1438,23 @@ 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_at(Instant::now()), 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
|
||||
);
|
||||
@@ -1275,7 +1471,10 @@ mod completion {
|
||||
fn a_live_request_is_not_expired_early() {
|
||||
let mut harness = Harness::new(1);
|
||||
let id = harness.coordinator.next_id;
|
||||
assert!(harness.coordinator.expire_at(Instant::now()).is_empty(), "not yet due");
|
||||
assert!(
|
||||
harness.coordinator.expire_at(Instant::now()).is_empty(),
|
||||
"not yet due"
|
||||
);
|
||||
assert!(harness.coordinator.is_live(id));
|
||||
assert!(harness.response().is_none(), "still unanswered, not failed");
|
||||
let remaining = harness
|
||||
@@ -1334,7 +1533,9 @@ mod completion {
|
||||
);
|
||||
}
|
||||
|
||||
fn observe_deadlines(coordinator: &mut Coordinator) -> std::rc::Rc<std::cell::RefCell<Vec<Option<Instant>>>> {
|
||||
fn observe_deadlines(
|
||||
coordinator: &mut Coordinator,
|
||||
) -> std::rc::Rc<std::cell::RefCell<Vec<Option<Instant>>>> {
|
||||
let changes = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
|
||||
let observed = changes.clone();
|
||||
coordinator.set_deadline_observer(move |deadline| observed.borrow_mut().push(deadline));
|
||||
@@ -1356,15 +1557,29 @@ mod completion {
|
||||
.admit_at(1, 42, reply.clone(), vec![spec(2, 2)], late)
|
||||
.unwrap();
|
||||
let second = coordinator
|
||||
.admit_at(2, 43, reply.clone(), vec![spec(2, 2)], late + Duration::from_secs(5))
|
||||
.admit_at(
|
||||
2,
|
||||
43,
|
||||
reply.clone(),
|
||||
vec![spec(2, 2)],
|
||||
late + Duration::from_secs(5),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
changes.borrow().len(),
|
||||
2,
|
||||
"a later request does not rearm the earliest timer"
|
||||
);
|
||||
assert!(coordinator.admit_at(1, 44, reply, Vec::new(), late).is_err());
|
||||
assert_eq!(changes.borrow().len(), 2, "rejected admission does not touch the timer");
|
||||
assert!(
|
||||
coordinator
|
||||
.admit_at(1, 44, reply, Vec::new(), late)
|
||||
.is_err()
|
||||
);
|
||||
assert_eq!(
|
||||
changes.borrow().len(),
|
||||
2,
|
||||
"rejected admission does not touch the timer"
|
||||
);
|
||||
assert_eq!(coordinator.terminate_owner(1), vec![first]);
|
||||
assert!(coordinator.is_live(second));
|
||||
assert_eq!(coordinator.terminate_owner(99), Vec::<u64>::new());
|
||||
@@ -1382,7 +1597,10 @@ mod completion {
|
||||
|
||||
#[test]
|
||||
fn deadline_survives_readback_and_encoding_then_disarms_on_completion() {
|
||||
for result in [Ok(PathBuf::from("/tmp/shot.png")), Err("encoder failed".into())] {
|
||||
for result in [
|
||||
Ok(PathBuf::from("/tmp/shot.png")),
|
||||
Err("encoder failed".into()),
|
||||
] {
|
||||
let mut harness = Harness::new(2);
|
||||
let changes = observe_deadlines(&mut harness.coordinator);
|
||||
let id = harness.coordinator.next_id;
|
||||
@@ -1392,11 +1610,19 @@ mod completion {
|
||||
harness.coordinator.on_part(id, 1, Ok(buffer(2, 2))),
|
||||
Action::Encode { .. }
|
||||
));
|
||||
assert_eq!(&*changes.borrow(), &[deadline], "encoding keeps the original deadline");
|
||||
assert_eq!(
|
||||
&*changes.borrow(),
|
||||
&[deadline],
|
||||
"encoding keeps the original deadline"
|
||||
);
|
||||
harness.coordinator.on_encoded(id, result);
|
||||
assert_eq!(&*changes.borrow(), &[deadline, None]);
|
||||
harness.coordinator.on_part(id, 0, Err("late readback".into()));
|
||||
harness.coordinator.on_encoded(id, Err("late encode".into()));
|
||||
harness
|
||||
.coordinator
|
||||
.on_part(id, 0, Err("late readback".into()));
|
||||
harness
|
||||
.coordinator
|
||||
.on_encoded(id, Err("late encode".into()));
|
||||
assert_eq!(
|
||||
changes.borrow().len(),
|
||||
2,
|
||||
@@ -1440,7 +1666,11 @@ mod completion {
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
assert_eq!(changes.borrow().last(), Some(&None), "{cancellation} must disarm");
|
||||
assert_eq!(
|
||||
changes.borrow().last(),
|
||||
Some(&None),
|
||||
"{cancellation} must disarm"
|
||||
);
|
||||
assert_eq!(changes.borrow().len(), 2);
|
||||
assert!(harness.response().unwrap().error.is_some());
|
||||
assert!(!harness.coordinator.is_live(id));
|
||||
@@ -1458,7 +1688,13 @@ mod completion {
|
||||
.admit_at(1, 42, reply.clone(), vec![spec(2, 2)], now)
|
||||
.unwrap();
|
||||
let second = coordinator
|
||||
.admit_at(2, 43, reply.clone(), vec![spec(2, 2)], now + Duration::from_secs(5))
|
||||
.admit_at(
|
||||
2,
|
||||
43,
|
||||
reply.clone(),
|
||||
vec![spec(2, 2)],
|
||||
now + Duration::from_secs(5),
|
||||
)
|
||||
.unwrap();
|
||||
coordinator.on_part(first, 0, Ok(buffer(2, 2)));
|
||||
assert!(
|
||||
@@ -1487,7 +1723,13 @@ mod completion {
|
||||
Action::DiscardFile(_)
|
||||
));
|
||||
coordinator
|
||||
.admit_at(3, 44, reply, vec![spec(2, 2)], now + Duration::from_secs(3600))
|
||||
.admit_at(
|
||||
3,
|
||||
44,
|
||||
reply,
|
||||
vec![spec(2, 2)],
|
||||
now + Duration::from_secs(3600),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
changes.borrow().last(),
|
||||
@@ -1505,7 +1747,10 @@ mod completion {
|
||||
assert!(!sink.publish(buffer(64, 64)));
|
||||
|
||||
let PartOutcome {
|
||||
request, part, result, ..
|
||||
request,
|
||||
part,
|
||||
result,
|
||||
..
|
||||
} = receiver.try_recv().expect("published once");
|
||||
assert_eq!((request, part), (7, 1));
|
||||
assert_eq!(result.unwrap().width, 2);
|
||||
@@ -1522,7 +1767,10 @@ mod completion {
|
||||
assert!(!sink.publish(buffer(2, 2)));
|
||||
|
||||
let PartOutcome {
|
||||
request, part, result, ..
|
||||
request,
|
||||
part,
|
||||
result,
|
||||
..
|
||||
} = receiver.try_recv().expect("answered");
|
||||
assert_eq!((request, part), (3, 0));
|
||||
assert!(result.is_err());
|
||||
@@ -1913,7 +2161,11 @@ 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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1945,7 +2197,11 @@ 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);
|
||||
}
|
||||
@@ -1974,7 +2230,11 @@ 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 {
|
||||
|
||||
@@ -113,8 +113,9 @@ fn encode_png(canvas: &Canvas) -> Result<Vec<u8>, String> {
|
||||
if width == 0 || height == 0 {
|
||||
return Err("Screenshot is empty".into());
|
||||
}
|
||||
let image = image::ImageBuffer::<image::Rgba<u8>, _>::from_raw(width, height, canvas.pixels.as_slice())
|
||||
.ok_or("Screenshot buffer does not match its dimensions")?;
|
||||
let image =
|
||||
image::ImageBuffer::<image::Rgba<u8>, _>::from_raw(width, height, canvas.pixels.as_slice())
|
||||
.ok_or("Screenshot buffer does not match its dimensions")?;
|
||||
|
||||
let mut out = Vec::new();
|
||||
let encoder = image::codecs::png::PngEncoder::new_with_quality(
|
||||
@@ -122,8 +123,14 @@ 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)
|
||||
}
|
||||
@@ -178,8 +185,8 @@ fn write_private_file_in(
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -194,7 +201,9 @@ 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}")),
|
||||
@@ -248,7 +257,11 @@ mod tests {
|
||||
.expect("decodes back")
|
||||
.to_rgba8();
|
||||
assert_eq!(decoded.dimensions(), (4, 3));
|
||||
assert_eq!(decoded.as_raw(), &canvas.pixels, "encoding changed the pixel data");
|
||||
assert_eq!(
|
||||
decoded.as_raw(),
|
||||
&canvas.pixels,
|
||||
"encoding changed the pixel data"
|
||||
);
|
||||
// The opaque alpha we force is preserved through the round trip.
|
||||
assert_eq!(decoded.get_pixel(0, 0)[3], 255);
|
||||
}
|
||||
@@ -279,7 +292,10 @@ 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();
|
||||
}
|
||||
|
||||
@@ -293,7 +309,10 @@ 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,7 +1,9 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use smithay::reexports::wayland_server::Client;
|
||||
use smithay::wayland::security_context::{SecurityContext, SecurityContextHandler, SecurityContextListenerSource};
|
||||
use smithay::wayland::security_context::{
|
||||
SecurityContext, SecurityContextHandler, SecurityContextListenerSource,
|
||||
};
|
||||
|
||||
use crate::Ferese;
|
||||
use crate::state::ClientState;
|
||||
@@ -14,15 +16,18 @@ pub(crate) fn unrestricted_client(client: &Client) -> bool {
|
||||
|
||||
impl SecurityContextHandler for Ferese {
|
||||
fn context_created(&mut self, source: SecurityContextListenerSource, context: SecurityContext) {
|
||||
if let Err(error) = self.loop_handle.insert_source(source, move |stream, _, state| {
|
||||
let data = ClientState {
|
||||
security_context: Some(context.clone()),
|
||||
..Default::default()
|
||||
};
|
||||
if let Err(error) = state.display_handle.insert_client(stream, Arc::new(data)) {
|
||||
tracing::warn!(%error, "failed to register sandboxed Wayland client");
|
||||
}
|
||||
}) {
|
||||
if let Err(error) = self
|
||||
.loop_handle
|
||||
.insert_source(source, move |stream, _, state| {
|
||||
let data = ClientState {
|
||||
security_context: Some(context.clone()),
|
||||
..Default::default()
|
||||
};
|
||||
if let Err(error) = state.display_handle.insert_client(stream, Arc::new(data)) {
|
||||
tracing::warn!(%error, "failed to register sandboxed Wayland client");
|
||||
}
|
||||
})
|
||||
{
|
||||
tracing::warn!(%error, "failed to register security context listener");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,19 +28,28 @@ fn icon_cleanup_messages(destroy_icon: bool) -> Vec<(u32, u16, Vec<u32>)> {
|
||||
.create_resource::<WlCompositor, GlobalData, Ferese>(&state.display_handle, 6, GlobalData)
|
||||
.unwrap();
|
||||
let shell = client
|
||||
.create_resource::<XdgWmBase, XdgWmBaseUserData, Ferese>(&state.display_handle, 6, Default::default())
|
||||
.create_resource::<XdgWmBase, XdgWmBaseUserData, Ferese>(
|
||||
&state.display_handle,
|
||||
6,
|
||||
Default::default(),
|
||||
)
|
||||
.unwrap();
|
||||
let shm = client
|
||||
.create_resource::<WlShm, GlobalData, Ferese>(&state.display_handle, 1, GlobalData)
|
||||
.unwrap();
|
||||
let manager = client
|
||||
.create_resource::<XdgToplevelIconManagerV1, GlobalData, Ferese>(&state.display_handle, 1, GlobalData)
|
||||
.create_resource::<XdgToplevelIconManagerV1, GlobalData, Ferese>(
|
||||
&state.display_handle,
|
||||
1,
|
||||
GlobalData,
|
||||
)
|
||||
.unwrap();
|
||||
request(&mut wire, compositor.id().protocol_id(), 0, &[2], None);
|
||||
request(&mut wire, shell.id().protocol_id(), 2, &[3, 2], None);
|
||||
request(&mut wire, 3, 1, &[4], None);
|
||||
let mut file = tempfile::tempfile().unwrap();
|
||||
file.write_all(&0xff112233u32.to_ne_bytes().repeat(32 * 32)).unwrap();
|
||||
file.write_all(&0xff112233u32.to_ne_bytes().repeat(32 * 32))
|
||||
.unwrap();
|
||||
request(
|
||||
&mut wire,
|
||||
shm.id().protocol_id(),
|
||||
@@ -85,9 +94,15 @@ fn destroying_an_assigned_icon_before_its_buffer_keeps_the_client_connected() {
|
||||
return;
|
||||
}
|
||||
let messages = icon_cleanup_messages(true);
|
||||
assert!(!messages.iter().any(|(object, opcode, _)| *object == 1 && *opcode == 0));
|
||||
assert!(
|
||||
messages.iter().any(|(object, opcode, _)| *object == 8 && *opcode == 0),
|
||||
!messages
|
||||
.iter()
|
||||
.any(|(object, opcode, _)| *object == 1 && *opcode == 0)
|
||||
);
|
||||
assert!(
|
||||
messages
|
||||
.iter()
|
||||
.any(|(object, opcode, _)| *object == 8 && *opcode == 0),
|
||||
"valid icon cleanup disconnected the client before its sync reply: {messages:?}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,9 @@ use smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::zwlr_scre
|
||||
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::reexports::wayland_server::{
|
||||
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
|
||||
};
|
||||
use smithay::utils::Rectangle;
|
||||
use smithay::wayland::shm::with_buffer_contents_mut;
|
||||
|
||||
@@ -34,7 +36,10 @@ 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;
|
||||
@@ -77,7 +82,11 @@ struct Conversion {
|
||||
|
||||
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.
|
||||
@@ -95,7 +104,10 @@ 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())
|
||||
@@ -178,11 +190,13 @@ fn finish_conversion(state: &Ferese, result: Conversion) {
|
||||
|
||||
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(
|
||||
@@ -248,7 +262,12 @@ impl Dispatch<FereseWindowCaptureManagerV1, Global> for Ferese {
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
*resource.data::<Arc<FrameData>>().unwrap().used.lock().unwrap() = true;
|
||||
*resource
|
||||
.data::<Arc<FrameData>>()
|
||||
.unwrap()
|
||||
.used
|
||||
.lock()
|
||||
.unwrap() = true;
|
||||
manager.busy(&resource);
|
||||
resource.failed();
|
||||
}
|
||||
@@ -287,7 +306,8 @@ 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()?)?
|
||||
@@ -299,26 +319,29 @@ impl Ferese {
|
||||
*busy = true;
|
||||
return None;
|
||||
};
|
||||
let cursor = if overlay_cursor && !self.overview.is_presenting() && !self.capture_cursor_protected() {
|
||||
self.seat.get_pointer().and_then(|pointer| {
|
||||
let location = pointer.current_location();
|
||||
if self.window_under_visual(location).as_ref() != Some(&window) {
|
||||
return None;
|
||||
}
|
||||
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)?;
|
||||
if pointer.current_focus().as_ref() != Some(&surface) {
|
||||
return None;
|
||||
}
|
||||
Some(Rectangle::new(
|
||||
(location - smithay::utils::Point::from((x, y))).to_i32_round(),
|
||||
geometry.size,
|
||||
))
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let cursor =
|
||||
if overlay_cursor && !self.overview.is_presenting() && !self.capture_cursor_protected()
|
||||
{
|
||||
self.seat.get_pointer().and_then(|pointer| {
|
||||
let location = pointer.current_location();
|
||||
if self.window_under_visual(location).as_ref() != Some(&window) {
|
||||
return None;
|
||||
}
|
||||
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)?;
|
||||
if pointer.current_focus().as_ref() != Some(&surface) {
|
||||
return None;
|
||||
}
|
||||
Some(Rectangle::new(
|
||||
(location - smithay::utils::Point::from((x, y))).to_i32_round(),
|
||||
geometry.size,
|
||||
))
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let result = if let Some(backend) = self.nested_backend.clone() {
|
||||
let mut backend = backend.try_borrow_mut().ok()?;
|
||||
crate::render::capture_window_frame(
|
||||
@@ -357,10 +380,18 @@ 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;
|
||||
@@ -393,7 +424,11 @@ pub(super) fn copy_snapshot(state: &Ferese, resource: &ZwlrScreencopyFrameV1, da
|
||||
// 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
|
||||
})
|
||||
@@ -459,7 +494,11 @@ pub(crate) mod tests {
|
||||
.unwrap();
|
||||
let data = frame_data();
|
||||
let frame = client
|
||||
.create_resource::<ZwlrScreencopyFrameV1, Arc<FrameData>, Ferese>(&state.display_handle, 3, data.clone())
|
||||
.create_resource::<ZwlrScreencopyFrameV1, Arc<FrameData>, Ferese>(
|
||||
&state.display_handle,
|
||||
3,
|
||||
data.clone(),
|
||||
)
|
||||
.unwrap();
|
||||
finish_conversion(
|
||||
state,
|
||||
@@ -475,20 +514,27 @@ pub(crate) mod tests {
|
||||
let data = frame_data();
|
||||
*data.snapshot.lock().unwrap() = Some(snapshot());
|
||||
let frame = client
|
||||
.create_resource::<ZwlrScreencopyFrameV1, Arc<FrameData>, Ferese>(&state.display_handle, 3, data.clone())
|
||||
.create_resource::<ZwlrScreencopyFrameV1, Arc<FrameData>, Ferese>(
|
||||
&state.display_handle,
|
||||
3,
|
||||
data.clone(),
|
||||
)
|
||||
.unwrap();
|
||||
let buffer = client
|
||||
.object_from_protocol_id::<WlBuffer>(&state.display_handle, 6)
|
||||
.unwrap();
|
||||
let before = with_buffer_contents_mut(&buffer, |ptr, _, layout| {
|
||||
// SAFETY: the fixture allocates and validates this 64x48 SHM buffer.
|
||||
unsafe { std::slice::from_raw_parts(ptr.add(layout.offset as usize), 64 * 48 * 4).to_vec() }
|
||||
unsafe {
|
||||
std::slice::from_raw_parts(ptr.add(layout.offset as usize), 64 * 48 * 4).to_vec()
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
copy_snapshot(state, &frame, &data, &buffer);
|
||||
with_buffer_contents_mut(&buffer, |ptr, _, layout| {
|
||||
// SAFETY: same fixture-owned buffer as above.
|
||||
let after = unsafe { std::slice::from_raw_parts(ptr.add(layout.offset as usize), 64 * 48 * 4) };
|
||||
let after =
|
||||
unsafe { std::slice::from_raw_parts(ptr.add(layout.offset as usize), 64 * 48 * 4) };
|
||||
if allowed {
|
||||
assert!(after.iter().all(|byte| *byte == 0));
|
||||
} else {
|
||||
@@ -496,7 +542,11 @@ pub(crate) mod tests {
|
||||
}
|
||||
// Restore the client's colored buffer for subsequent renderer checks.
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(before.as_ptr(), ptr.add(layout.offset as usize), before.len());
|
||||
std::ptr::copy_nonoverlapping(
|
||||
before.as_ptr(),
|
||||
ptr.add(layout.offset as usize),
|
||||
before.len(),
|
||||
);
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
@@ -60,7 +60,9 @@ impl XdgShellHandler for Ferese {
|
||||
fn parent_changed(&mut self, surface: ToplevelSurface) {
|
||||
self.refresh_capture_privacy();
|
||||
self.refresh_idle_inhibition();
|
||||
let parent = surface.parent().and_then(|parent| self.windows.id_for_surface(&parent));
|
||||
let parent = surface
|
||||
.parent()
|
||||
.and_then(|parent| self.windows.id_for_surface(&parent));
|
||||
let child = self
|
||||
.windows
|
||||
.id_for_surface(surface.wl_surface())
|
||||
@@ -95,7 +97,12 @@ 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;
|
||||
@@ -252,7 +259,12 @@ 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);
|
||||
@@ -264,14 +276,21 @@ 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 {
|
||||
@@ -325,12 +344,14 @@ pub(crate) fn validate_size_constraints(toplevel: &ToplevelSurface) -> bool {
|
||||
let mut cached = states.cached_state.get::<SurfaceCachedState>();
|
||||
let state = cached.current();
|
||||
let valid_axis = |min: i32, max: i32| min >= 0 && max >= 0 && (max == 0 || min <= max);
|
||||
valid_axis(state.min_size.w, state.max_size.w) && valid_axis(state.min_size.h, state.max_size.h)
|
||||
valid_axis(state.min_size.w, state.max_size.w)
|
||||
&& valid_axis(state.min_size.h, state.max_size.h)
|
||||
});
|
||||
if !valid {
|
||||
toplevel
|
||||
.xdg_toplevel()
|
||||
.post_error(xdg_toplevel::Error::InvalidSize, "invalid minimum/maximum window size");
|
||||
toplevel.xdg_toplevel().post_error(
|
||||
xdg_toplevel::Error::InvalidSize,
|
||||
"invalid minimum/maximum window size",
|
||||
);
|
||||
}
|
||||
|
||||
valid
|
||||
@@ -359,7 +380,9 @@ pub fn handle_commit(state: &mut Ferese, surface: &WlSurface) {
|
||||
if let Some(PopupKind::Xdg(popup)) = state.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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -415,10 +438,11 @@ 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 id = *self.windows.ids().get(window)?;
|
||||
@@ -432,11 +456,14 @@ impl Ferese {
|
||||
|
||||
Some((output_geometry, location))
|
||||
} else {
|
||||
let layer = self.space.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL)?;
|
||||
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))
|
||||
})
|
||||
@@ -490,7 +517,9 @@ impl Dispatch<xdg_toplevel::XdgToplevel, XdgShellSurfaceUserData> for Ferese {
|
||||
return;
|
||||
}
|
||||
|
||||
smithay::wayland::Dispatch2::request(data, state, client, resource, request, dhandle, data_init);
|
||||
smithay::wayland::Dispatch2::request(
|
||||
data, state, client, resource, request, dhandle, data_init,
|
||||
);
|
||||
}
|
||||
|
||||
fn destroyed(
|
||||
@@ -524,7 +553,11 @@ fn size_limits_usable(
|
||||
(pending.max_size.w, pending.max_size.h),
|
||||
)
|
||||
});
|
||||
let (min, max) = if minimum { (proposed, max) } else { (min, proposed) };
|
||||
let (min, max) = if minimum {
|
||||
(proposed, max)
|
||||
} else {
|
||||
(min, proposed)
|
||||
};
|
||||
(max.0 == 0 || min.0 <= max.0) && (max.1 == 0 || min.1 <= max.1)
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user