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
+113
-281
@@ -1,57 +1,41 @@
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
error::Error,
|
||||
io,
|
||||
path::Path,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::error::Error;
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use smithay::{
|
||||
backend::{
|
||||
allocator::{
|
||||
Fourcc,
|
||||
gbm::{GbmAllocator, GbmBufferFlags, GbmDevice},
|
||||
},
|
||||
drm::{
|
||||
DrmDevice, DrmDeviceFd, DrmEvent, DrmEventTime, DrmNode, GbmBufferedSurface, NodeType,
|
||||
},
|
||||
egl::{EGLContext, EGLDisplay},
|
||||
input::InputEvent,
|
||||
libinput::{LibinputInputBackend, LibinputSessionInterface},
|
||||
renderer::{
|
||||
Bind, Frame, ImportDma, ImportMemWl, Renderer, damage::OutputDamageTracker,
|
||||
gles::GlesRenderer,
|
||||
},
|
||||
session::{Event as SessionEvent, Session, libseat::LibSeatSession},
|
||||
udev::{UdevBackend, UdevEvent, primary_gpu},
|
||||
},
|
||||
desktop::{layer_map_for_output, utils::OutputPresentationFeedback},
|
||||
output::{Mode as OutputMode, Output, PhysicalProperties, Scale, Subpixel},
|
||||
reexports::{
|
||||
calloop::{
|
||||
EventLoop, RegistrationToken,
|
||||
timer::{TimeoutAction, Timer},
|
||||
},
|
||||
drm::control::{
|
||||
Device as ControlDevice, Mode as DrmMode, ModeTypeFlags, connector, crtc, property,
|
||||
},
|
||||
input::{Device as LibinputDevice, Libinput},
|
||||
rustix::fs::OFlags,
|
||||
wayland_protocols::wp::presentation_time::server::wp_presentation_feedback::Kind,
|
||||
wayland_server::backend::GlobalId,
|
||||
},
|
||||
utils::{DeviceFd, Monotonic, Time, Transform},
|
||||
wayland::{dmabuf::DmabufFeedbackBuilder, presentation::Refresh},
|
||||
use smithay::backend::allocator::Fourcc;
|
||||
use smithay::backend::allocator::gbm::{GbmAllocator, GbmBufferFlags, GbmDevice};
|
||||
use smithay::backend::drm::{DrmDevice, DrmDeviceFd, DrmEvent, DrmEventTime, DrmNode, GbmBufferedSurface, NodeType};
|
||||
use smithay::backend::egl::{EGLContext, EGLDisplay};
|
||||
use smithay::backend::input::InputEvent;
|
||||
use smithay::backend::libinput::{LibinputInputBackend, LibinputSessionInterface};
|
||||
use smithay::backend::renderer::damage::OutputDamageTracker;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::backend::renderer::{Bind, Frame, ImportDma, ImportMemWl, Renderer};
|
||||
use smithay::backend::session::libseat::LibSeatSession;
|
||||
use smithay::backend::session::{Event as SessionEvent, Session};
|
||||
use smithay::backend::udev::{UdevBackend, UdevEvent, primary_gpu};
|
||||
use smithay::desktop::layer_map_for_output;
|
||||
use smithay::desktop::utils::OutputPresentationFeedback;
|
||||
use smithay::output::{Mode as OutputMode, Output, PhysicalProperties, Scale, Subpixel};
|
||||
use smithay::reexports::calloop::timer::{TimeoutAction, Timer};
|
||||
use smithay::reexports::calloop::{EventLoop, RegistrationToken};
|
||||
use smithay::reexports::drm::control::{
|
||||
Device as ControlDevice, Mode as DrmMode, ModeTypeFlags, connector, crtc, property,
|
||||
};
|
||||
use smithay::reexports::input::{Device as LibinputDevice, Libinput};
|
||||
use smithay::reexports::rustix::fs::OFlags;
|
||||
use smithay::reexports::wayland_protocols::wp::presentation_time::server::wp_presentation_feedback::Kind;
|
||||
use smithay::reexports::wayland_server::backend::GlobalId;
|
||||
use smithay::utils::{DeviceFd, Monotonic, Time, Transform};
|
||||
use smithay::wayland::dmabuf::DmabufFeedbackBuilder;
|
||||
use smithay::wayland::presentation::Refresh;
|
||||
|
||||
use crate::{
|
||||
Ferese,
|
||||
config::{OutputModeRequest, OutputProfile, OutputSettings, OutputTransform},
|
||||
metrics::RenderMetrics,
|
||||
winit::{
|
||||
animated_window_elements, cursorless_window_elements, frame_effect_metrics, redraw_output,
|
||||
},
|
||||
};
|
||||
use crate::Ferese;
|
||||
use crate::config::{OutputModeRequest, OutputProfile, OutputSettings, OutputTransform};
|
||||
use crate::metrics::RenderMetrics;
|
||||
use crate::winit::{animated_window_elements, cursorless_window_elements, frame_effect_metrics, redraw_output};
|
||||
|
||||
pub struct DirectBackendState {
|
||||
pub session: LibSeatSession,
|
||||
@@ -72,14 +56,12 @@ impl DirectBackendState {
|
||||
geometry: smithay::utils::Rectangle<i32, smithay::utils::Logical>,
|
||||
output: &Output,
|
||||
remaining: usize,
|
||||
) -> Result<Option<crate::winit::ResizeSnapshot>, smithay::backend::renderer::gles::GlesError>
|
||||
{
|
||||
let Some(device) = self.devices.values_mut().find(|device| {
|
||||
device
|
||||
.outputs
|
||||
.values()
|
||||
.any(|candidate| &candidate.output == output)
|
||||
}) else {
|
||||
) -> Result<Option<crate::winit::ResizeSnapshot>, smithay::backend::renderer::gles::GlesError> {
|
||||
let Some(device) = self
|
||||
.devices
|
||||
.values_mut()
|
||||
.find(|device| device.outputs.values().any(|candidate| &candidate.output == output))
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
crate::winit::capture_resize_snapshot(
|
||||
@@ -100,12 +82,7 @@ impl DirectBackendState {
|
||||
let device = self
|
||||
.devices
|
||||
.values_mut()
|
||||
.find(|device| {
|
||||
device
|
||||
.outputs
|
||||
.values()
|
||||
.any(|candidate| &candidate.output == output)
|
||||
})
|
||||
.find(|device| device.outputs.values().any(|candidate| &candidate.output == output))
|
||||
.ok_or("Window output is unavailable")?;
|
||||
crate::winit::capture_window_buffer(
|
||||
&mut device.renderer,
|
||||
@@ -124,12 +101,7 @@ impl DirectBackendState {
|
||||
let device = self
|
||||
.devices
|
||||
.values_mut()
|
||||
.find(|device| {
|
||||
device
|
||||
.outputs
|
||||
.values()
|
||||
.any(|candidate| &candidate.output == output)
|
||||
})
|
||||
.find(|device| device.outputs.values().any(|candidate| &candidate.output == output))
|
||||
.ok_or("Window output is unavailable")?;
|
||||
crate::winit::capture_window_frame(
|
||||
&mut device.renderer,
|
||||
@@ -212,12 +184,7 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
|
||||
let seat_name = session.seat();
|
||||
let udev_backend = UdevBackend::new(&seat_name)?;
|
||||
let primary_path = primary_gpu(&seat_name)?
|
||||
.or_else(|| {
|
||||
udev_backend
|
||||
.device_list()
|
||||
.next()
|
||||
.map(|(_, path)| path.to_owned())
|
||||
})
|
||||
.or_else(|| udev_backend.device_list().next().map(|(_, path)| path.to_owned()))
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "no DRM device found"))?;
|
||||
let mut libinput_context =
|
||||
Libinput::new_with_udev::<LibinputSessionInterface<LibSeatSession>>(session.clone().into());
|
||||
@@ -254,9 +221,7 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
|
||||
}
|
||||
InputEvent::DeviceRemoved { device } => {
|
||||
if let Some(backend) = state.direct_backend.as_mut() {
|
||||
backend
|
||||
.input_devices
|
||||
.retain(|candidate| candidate != &device);
|
||||
backend.input_devices.retain(|candidate| candidate != &device);
|
||||
}
|
||||
}
|
||||
event => state.process_input_event(event),
|
||||
@@ -275,8 +240,7 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
|
||||
device.drm.pause();
|
||||
device.outputs.values_mut().for_each(|output| {
|
||||
output.surface.reset_buffers();
|
||||
output.damage_tracker =
|
||||
OutputDamageTracker::from_output(&output.output);
|
||||
output.damage_tracker = OutputDamageTracker::from_output(&output.output);
|
||||
output.frame_pending = false;
|
||||
output.power_off = false;
|
||||
});
|
||||
@@ -335,20 +299,12 @@ fn configure_libinput_device(state: &Ferese, device: &mut LibinputDevice) {
|
||||
if device.config_tap_finger_count() > 0
|
||||
&& let Err(error) = device.config_tap_set_enabled(settings.tap)
|
||||
{
|
||||
tracing::warn!(
|
||||
device = device.name(),
|
||||
?error,
|
||||
"failed to configure tap-to-click"
|
||||
);
|
||||
tracing::warn!(device = device.name(), ?error, "failed to configure tap-to-click");
|
||||
}
|
||||
if device.config_scroll_has_natural_scroll()
|
||||
&& let Err(error) = device.config_scroll_set_natural_scroll_enabled(settings.natural_scroll)
|
||||
{
|
||||
tracing::warn!(
|
||||
device = device.name(),
|
||||
?error,
|
||||
"failed to configure natural scrolling"
|
||||
);
|
||||
tracing::warn!(device = device.name(), ?error, "failed to configure natural scrolling");
|
||||
}
|
||||
if device.config_dwt_is_available()
|
||||
&& let Err(error) = device.config_dwt_set_enabled(settings.disable_while_typing)
|
||||
@@ -383,10 +339,7 @@ pub(crate) fn reload_outputs(state: &mut Ferese) {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn validate_live_outputs(
|
||||
state: &Ferese,
|
||||
profiles: &[OutputProfile],
|
||||
) -> Result<(), String> {
|
||||
pub(crate) fn validate_live_outputs(state: &Ferese, profiles: &[OutputProfile]) -> Result<(), String> {
|
||||
let Some(backend) = state.direct_backend.as_ref() else {
|
||||
return Ok(());
|
||||
};
|
||||
@@ -414,10 +367,7 @@ fn open_primary_device(
|
||||
.session
|
||||
.clone();
|
||||
let node = DrmNode::from_path(path)?;
|
||||
let fd = session.open(
|
||||
path,
|
||||
OFlags::RDWR | OFlags::CLOEXEC | OFlags::NOCTTY | OFlags::NONBLOCK,
|
||||
)?;
|
||||
let fd = session.open(path, OFlags::RDWR | OFlags::CLOEXEC | OFlags::NOCTTY | OFlags::NONBLOCK)?;
|
||||
let fd = DrmDeviceFd::new(DeviceFd::from(fd));
|
||||
let (mut drm, notifier) = DrmDevice::new(fd.clone(), true)?;
|
||||
let gbm = GbmDevice::new(fd)?;
|
||||
@@ -443,10 +393,7 @@ fn open_primary_device(
|
||||
let mut scan = select_outputs(&drm, &state.output_profiles)?;
|
||||
apply_lid_policy(
|
||||
&mut scan,
|
||||
state
|
||||
.direct_backend
|
||||
.as_ref()
|
||||
.is_some_and(|backend| backend.lid_closed),
|
||||
state.direct_backend.as_ref().is_some_and(|backend| backend.lid_closed),
|
||||
);
|
||||
if scan.selections.is_empty() {
|
||||
return Err(io::Error::new(
|
||||
@@ -468,9 +415,7 @@ fn open_primary_device(
|
||||
mode,
|
||||
settings,
|
||||
} = selection;
|
||||
let output = create_direct_output(
|
||||
state, &mut drm, &gbm, &renderer, connector, crtc, mode, &settings,
|
||||
)?;
|
||||
let output = create_direct_output(state, &mut drm, &gbm, &renderer, connector, crtc, mode, &settings)?;
|
||||
state
|
||||
.direct_backend
|
||||
.as_mut()
|
||||
@@ -503,9 +448,7 @@ fn open_primary_device(
|
||||
None
|
||||
}
|
||||
});
|
||||
if let (Some(mut feedback), DrmEventTime::Monotonic(time)) =
|
||||
(feedback, metadata.time)
|
||||
{
|
||||
if let (Some(mut feedback), DrmEventTime::Monotonic(time)) = (feedback, metadata.time) {
|
||||
feedback.presented(
|
||||
Time::<Monotonic>::from(time),
|
||||
Refresh::fixed(refresh_duration(state, node, crtc)),
|
||||
@@ -538,10 +481,7 @@ fn open_primary_device(
|
||||
}
|
||||
state.record_drm_presentation(node, crtc, metadata.time, metadata.sequence);
|
||||
tracing::trace!(?node, ?crtc, sequence = metadata.sequence, "page flip");
|
||||
let active = state
|
||||
.direct_backend
|
||||
.as_ref()
|
||||
.is_some_and(|backend| backend.active);
|
||||
let active = state.direct_backend.as_ref().is_some_and(|backend| backend.active);
|
||||
if active {
|
||||
let was_animating = state
|
||||
.direct_backend
|
||||
@@ -588,11 +528,7 @@ pub(crate) fn sleep_locked_outputs(state: &mut Ferese) {
|
||||
let Some(backend) = state.direct_backend.as_mut() else {
|
||||
return;
|
||||
};
|
||||
for device in backend
|
||||
.devices
|
||||
.values_mut()
|
||||
.filter(|device| device.drm.is_active())
|
||||
{
|
||||
for device in backend.devices.values_mut().filter(|device| device.drm.is_active()) {
|
||||
for output in device.outputs.values_mut() {
|
||||
if output.power_off || output.frame_pending {
|
||||
continue;
|
||||
@@ -621,11 +557,7 @@ pub(crate) fn wake_locked_outputs(state: &mut Ferese) {
|
||||
}
|
||||
|
||||
pub fn render_all(state: &mut Ferese) {
|
||||
if state
|
||||
.direct_backend
|
||||
.as_ref()
|
||||
.is_none_or(|backend| !backend.active)
|
||||
{
|
||||
if state.direct_backend.as_ref().is_none_or(|backend| !backend.active) {
|
||||
return;
|
||||
}
|
||||
let animating = state.advance_animations(Instant::now());
|
||||
@@ -692,9 +624,7 @@ fn initial_lid_closed() -> bool {
|
||||
fn internal_connector(interface: connector::Interface) -> bool {
|
||||
matches!(
|
||||
interface,
|
||||
connector::Interface::EmbeddedDisplayPort
|
||||
| connector::Interface::LVDS
|
||||
| connector::Interface::DSI
|
||||
connector::Interface::EmbeddedDisplayPort | connector::Interface::LVDS | connector::Interface::DSI
|
||||
)
|
||||
}
|
||||
|
||||
@@ -767,8 +697,7 @@ fn render_output(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle) {
|
||||
state.process_dmabuf_imports(&mut device.renderer);
|
||||
let (mut buffer, age) = output.surface.next_buffer()?;
|
||||
let elements = animated_window_elements(state, &mut device.renderer, &output.output);
|
||||
let effects =
|
||||
frame_effect_metrics(&elements, output.output.current_scale().fractional_scale());
|
||||
let effects = frame_effect_metrics(&elements, output.output.current_scale().fractional_scale());
|
||||
let mut framebuffer = device.renderer.bind(&mut buffer)?;
|
||||
let result = output.damage_tracker.render_output(
|
||||
&mut device.renderer,
|
||||
@@ -778,12 +707,10 @@ fn render_output(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle) {
|
||||
[0.035, 0.04, 0.055, 1.0],
|
||||
)?;
|
||||
let cursorless_capture = state.has_pending_screencopy(&output.output, false);
|
||||
let captured_with_cursor =
|
||||
state.process_screencopies(&mut device.renderer, &framebuffer, &output.output, true);
|
||||
let captured_with_cursor = state.process_screencopies(&mut device.renderer, &framebuffer, &output.output, true);
|
||||
|
||||
if cursorless_capture {
|
||||
let cursorless_elements =
|
||||
cursorless_window_elements(state, &mut device.renderer, &output.output);
|
||||
let cursorless_elements = cursorless_window_elements(state, &mut device.renderer, &output.output);
|
||||
redraw_output(
|
||||
&mut device.renderer,
|
||||
&mut framebuffer,
|
||||
@@ -791,22 +718,13 @@ fn render_output(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle) {
|
||||
&cursorless_elements,
|
||||
)?;
|
||||
state.process_screencopies(&mut device.renderer, &framebuffer, &output.output, false);
|
||||
redraw_output(
|
||||
&mut device.renderer,
|
||||
&mut framebuffer,
|
||||
&output.output,
|
||||
&elements,
|
||||
)?;
|
||||
redraw_output(&mut device.renderer, &mut framebuffer, &output.output, &elements)?;
|
||||
} else if captured_with_cursor {
|
||||
let _ = device
|
||||
.renderer
|
||||
.render(
|
||||
&mut framebuffer,
|
||||
output
|
||||
.output
|
||||
.current_mode()
|
||||
.expect("output has a mode")
|
||||
.size,
|
||||
output.output.current_mode().expect("output has a mode").size,
|
||||
output.output.current_transform(),
|
||||
)?
|
||||
.finish()?;
|
||||
@@ -818,34 +736,18 @@ fn render_output(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle) {
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
let mut presentation = OutputPresentationFeedback::new(&output.output);
|
||||
state.space.elements().for_each(|window| {
|
||||
window.take_presentation_feedback(
|
||||
&mut presentation,
|
||||
|_, _| Some(output.output.clone()),
|
||||
|_, _| Kind::Vsync | Kind::HwClock | Kind::HwCompletion,
|
||||
);
|
||||
});
|
||||
let layers = layer_map_for_output(&output.output)
|
||||
.layers()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
layers.iter().for_each(|layer| {
|
||||
layer.take_presentation_feedback(
|
||||
&mut presentation,
|
||||
|_, _| Some(output.output.clone()),
|
||||
|_, _| Kind::Vsync | Kind::HwClock | Kind::HwCompletion,
|
||||
);
|
||||
});
|
||||
let presentation = crate::presentation::take_output_feedback(
|
||||
state,
|
||||
&output.output,
|
||||
&result.states,
|
||||
Kind::Vsync | Kind::HwClock | Kind::HwCompletion,
|
||||
);
|
||||
output
|
||||
.surface
|
||||
.queue_buffer(Some(result.sync), Some(damage.clone()), presentation)?;
|
||||
output.render_metrics.record_frame(
|
||||
render_started.elapsed(),
|
||||
&damage,
|
||||
missed_deadlines,
|
||||
effects,
|
||||
);
|
||||
output
|
||||
.render_metrics
|
||||
.record_frame(render_started.elapsed(), &damage, missed_deadlines, effects);
|
||||
output.frame_pending = true;
|
||||
output.lock_frame_pending = state.session_lock.active;
|
||||
Ok(true)
|
||||
@@ -893,10 +795,7 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
let lid_closed = state
|
||||
.direct_backend
|
||||
.as_ref()
|
||||
.is_some_and(|backend| backend.lid_closed);
|
||||
let lid_closed = state.direct_backend.as_ref().is_some_and(|backend| backend.lid_closed);
|
||||
apply_lid_policy(&mut scan, lid_closed);
|
||||
if let Some(backend) = state.direct_backend.as_mut() {
|
||||
backend.connected_outputs = scan.connected_outputs;
|
||||
@@ -940,9 +839,7 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) {
|
||||
Some(Scale::Fractional(output.settings.scale)),
|
||||
Some((position[0], position[1]).into()),
|
||||
);
|
||||
state
|
||||
.space
|
||||
.map_output(&output.output, (position[0], position[1]));
|
||||
state.space.map_output(&output.output, (position[0], position[1]));
|
||||
if let Some(id) = state.output_id(&output.output)
|
||||
&& let Some(g) = state.space.output_geometry(&output.output)
|
||||
{
|
||||
@@ -1045,10 +942,7 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) {
|
||||
}
|
||||
|
||||
for crtc in deferred_removals {
|
||||
let keep_internal = device
|
||||
.outputs
|
||||
.get(&crtc)
|
||||
.is_some_and(|output| output.internal)
|
||||
let keep_internal = device.outputs.get(&crtc).is_some_and(|output| output.internal)
|
||||
&& lid_closed
|
||||
&& state.direct_backend.as_ref().is_some_and(|backend| {
|
||||
device.outputs.get(&crtc).is_some_and(|output| {
|
||||
@@ -1060,10 +954,7 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) {
|
||||
})
|
||||
&& !device.outputs.values().any(|output| !output.internal);
|
||||
if keep_internal {
|
||||
tracing::warn!(
|
||||
?crtc,
|
||||
"keeping laptop panel enabled: external output activation failed"
|
||||
);
|
||||
tracing::warn!(?crtc, "keeping laptop panel enabled: external output activation failed");
|
||||
if let Some(output) = device.outputs.get(&crtc)
|
||||
&& let Some(backend) = state.direct_backend.as_mut()
|
||||
&& let Some(info) = backend
|
||||
@@ -1116,9 +1007,7 @@ fn remove_device(state: &mut Ferese, node: DrmNode) {
|
||||
|
||||
let context = device.renderer.context_id().erased();
|
||||
state.wallpaper.forget_context(&context);
|
||||
state
|
||||
.resize_snapshots
|
||||
.retain(|_, snapshot| snapshot.context != context);
|
||||
state.resize_snapshots.retain(|_, snapshot| snapshot.context != context);
|
||||
for (crtc, output) in device.outputs {
|
||||
state.display_handle.disable_global::<Ferese>(output.global);
|
||||
state.unregister_output(&output.output);
|
||||
@@ -1170,9 +1059,10 @@ fn arm_animation_timer(state: &mut Ferese) {
|
||||
match state
|
||||
.loop_handle
|
||||
.insert_source(Timer::from_deadline(deadline), |_, _, state| {
|
||||
let active = state.direct_backend.as_ref().is_some_and(|backend| {
|
||||
backend.active && backend.animation_active && !state.session_lock.sleeping
|
||||
});
|
||||
let active = state
|
||||
.direct_backend
|
||||
.as_ref()
|
||||
.is_some_and(|backend| backend.active && backend.animation_active && !state.session_lock.sleeping);
|
||||
if active {
|
||||
let deadline = state.animation_fallback_deadline(animation_interval(state));
|
||||
if Instant::now() < deadline {
|
||||
@@ -1184,9 +1074,7 @@ fn arm_animation_timer(state: &mut Ferese) {
|
||||
.as_ref()
|
||||
.is_some_and(|backend| backend.animation_active)
|
||||
{
|
||||
return TimeoutAction::ToInstant(
|
||||
state.animation_fallback_deadline(animation_interval(state)),
|
||||
);
|
||||
return TimeoutAction::ToInstant(state.animation_fallback_deadline(animation_interval(state)));
|
||||
}
|
||||
}
|
||||
if let Some(backend) = state.direct_backend.as_mut() {
|
||||
@@ -1244,12 +1132,7 @@ fn schedule_frame_callbacks(state: &mut Ferese, node: DrmNode, crtc: crtc::Handl
|
||||
}
|
||||
}
|
||||
|
||||
fn deliver_frame_callbacks(
|
||||
state: &mut Ferese,
|
||||
node: DrmNode,
|
||||
crtc: crtc::Handle,
|
||||
identity: &Output,
|
||||
) {
|
||||
fn deliver_frame_callbacks(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle, identity: &Output) {
|
||||
let Some(backend) = state.direct_backend.as_mut() else {
|
||||
return;
|
||||
};
|
||||
@@ -1286,24 +1169,15 @@ fn send_frame_callbacks(state: &mut Ferese, output: &Output) {
|
||||
.is_some_and(|id| state.window_belongs_to_output(*id, output))
|
||||
})
|
||||
.for_each(|window| {
|
||||
window.send_frame(
|
||||
output,
|
||||
state.start_time.elapsed(),
|
||||
Some(Duration::ZERO),
|
||||
|_, _| Some(output.clone()),
|
||||
);
|
||||
window.send_frame(output, state.start_time.elapsed(), Some(Duration::ZERO), |_, _| {
|
||||
Some(output.clone())
|
||||
});
|
||||
});
|
||||
let layers = layer_map_for_output(output)
|
||||
.layers()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
let layers = layer_map_for_output(output).layers().cloned().collect::<Vec<_>>();
|
||||
layers.iter().for_each(|layer| {
|
||||
layer.send_frame(
|
||||
output,
|
||||
state.start_time.elapsed(),
|
||||
Some(Duration::ZERO),
|
||||
|_, _| Some(output.clone()),
|
||||
);
|
||||
layer.send_frame(output, state.start_time.elapsed(), Some(Duration::ZERO), |_, _| {
|
||||
Some(output.clone())
|
||||
});
|
||||
});
|
||||
state.send_cursor_frame(output);
|
||||
state.space.refresh();
|
||||
@@ -1365,12 +1239,7 @@ fn select_outputs(drm: &DrmDevice, profiles: &[OutputProfile]) -> io::Result<Out
|
||||
profile: active_profile.map(|profile| profile.name.clone()),
|
||||
physical_size: connector.size(),
|
||||
current_mode: selected_mode.map(connected_mode_info),
|
||||
available_modes: connector
|
||||
.modes()
|
||||
.iter()
|
||||
.copied()
|
||||
.map(connected_mode_info)
|
||||
.collect(),
|
||||
available_modes: connector.modes().iter().copied().map(connected_mode_info).collect(),
|
||||
scale: settings.scale,
|
||||
transform: settings.transform,
|
||||
configured_position: settings.position,
|
||||
@@ -1438,10 +1307,7 @@ fn default_output_settings(matcher: String) -> OutputSettings {
|
||||
}
|
||||
}
|
||||
|
||||
fn select_mode(
|
||||
connector: &connector::Info,
|
||||
requested: Option<OutputModeRequest>,
|
||||
) -> Option<DrmMode> {
|
||||
fn select_mode(connector: &connector::Info, requested: Option<OutputModeRequest>) -> Option<DrmMode> {
|
||||
let fallback = || {
|
||||
connector
|
||||
.modes()
|
||||
@@ -1460,24 +1326,11 @@ fn select_mode(
|
||||
.filter(|mode| mode.size() == (requested.width, requested.height));
|
||||
let selected = if let Some(refresh) = requested.refresh_millihertz {
|
||||
matching
|
||||
.min_by_key(|mode| {
|
||||
OutputMode::from(**mode)
|
||||
.refresh
|
||||
.unsigned_abs()
|
||||
.abs_diff(refresh)
|
||||
})
|
||||
.filter(|mode| {
|
||||
OutputMode::from(**mode)
|
||||
.refresh
|
||||
.unsigned_abs()
|
||||
.abs_diff(refresh)
|
||||
<= 1_000
|
||||
})
|
||||
.min_by_key(|mode| OutputMode::from(**mode).refresh.unsigned_abs().abs_diff(refresh))
|
||||
.filter(|mode| OutputMode::from(**mode).refresh.unsigned_abs().abs_diff(refresh) <= 1_000)
|
||||
.copied()
|
||||
} else {
|
||||
matching
|
||||
.max_by_key(|mode| OutputMode::from(**mode).refresh)
|
||||
.copied()
|
||||
matching.max_by_key(|mode| OutputMode::from(**mode).refresh).copied()
|
||||
};
|
||||
if selected.is_none() {
|
||||
tracing::warn!(
|
||||
@@ -1504,10 +1357,7 @@ fn create_direct_output(
|
||||
) -> Result<DirectOutput, Box<dyn Error>> {
|
||||
let identity = connector_identity(drm, &connector);
|
||||
let drm_surface = drm.create_surface(crtc, mode, &[connector.handle()])?;
|
||||
let allocator = GbmAllocator::new(
|
||||
gbm.clone(),
|
||||
GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT,
|
||||
);
|
||||
let allocator = GbmAllocator::new(gbm.clone(), GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT);
|
||||
let surface = GbmBufferedSurface::new(
|
||||
drm_surface,
|
||||
allocator,
|
||||
@@ -1602,21 +1452,18 @@ fn output_transform(transform: OutputTransform) -> Transform {
|
||||
}
|
||||
|
||||
fn connector_identity(drm: &DrmDevice, connector: &connector::Info) -> String {
|
||||
let edid = drm
|
||||
.get_properties(connector.handle())
|
||||
.ok()
|
||||
.and_then(|properties| {
|
||||
properties.iter().find_map(|(handle, raw)| {
|
||||
let info = drm.get_property(*handle).ok()?;
|
||||
if info.name().to_bytes() != b"EDID" {
|
||||
return None;
|
||||
}
|
||||
match info.value_type().convert_value(*raw) {
|
||||
property::Value::Blob(blob) if blob != 0 => drm.get_property_blob(blob).ok(),
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
});
|
||||
let edid = drm.get_properties(connector.handle()).ok().and_then(|properties| {
|
||||
properties.iter().find_map(|(handle, raw)| {
|
||||
let info = drm.get_property(*handle).ok()?;
|
||||
if info.name().to_bytes() != b"EDID" {
|
||||
return None;
|
||||
}
|
||||
match info.value_type().convert_value(*raw) {
|
||||
property::Value::Blob(blob) if blob != 0 => drm.get_property_blob(blob).ok(),
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
match edid {
|
||||
Some(edid) if !edid.is_empty() => format!("drm-edid:{:016x}", stable_hash(&edid)),
|
||||
@@ -1654,23 +1501,16 @@ impl PresentationClock {
|
||||
|
||||
fn set_refresh(&mut self, refresh_millihertz: i32) {
|
||||
if refresh_millihertz > 0 {
|
||||
self.refresh_interval = Some(Duration::from_nanos(
|
||||
1_000_000_000_000_u64 / refresh_millihertz as u64,
|
||||
));
|
||||
self.refresh_interval = Some(Duration::from_nanos(1_000_000_000_000_u64 / refresh_millihertz as u64));
|
||||
}
|
||||
}
|
||||
|
||||
fn record(&mut self, time: DrmEventTime, sequence: u32) -> Option<Duration> {
|
||||
let DrmEventTime::Monotonic(time) = time else {
|
||||
tracing::warn!(
|
||||
sequence,
|
||||
"DRM driver reported a realtime page-flip timestamp"
|
||||
);
|
||||
tracing::warn!(sequence, "DRM driver reported a realtime page-flip timestamp");
|
||||
return None;
|
||||
};
|
||||
let delta = self
|
||||
.last_presentation
|
||||
.map(|previous| time.saturating_sub(previous));
|
||||
let delta = self.last_presentation.map(|previous| time.saturating_sub(previous));
|
||||
|
||||
self.last_presentation = Some(time);
|
||||
self.presented_frames = self.presented_frames.saturating_add(1);
|
||||
@@ -1709,9 +1549,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn panel_detection_uses_connector_type_not_monitor_name() {
|
||||
assert!(internal_connector(
|
||||
connector::Interface::EmbeddedDisplayPort
|
||||
));
|
||||
assert!(internal_connector(connector::Interface::EmbeddedDisplayPort));
|
||||
assert!(internal_connector(connector::Interface::LVDS));
|
||||
assert!(internal_connector(connector::Interface::DSI));
|
||||
assert!(!internal_connector(connector::Interface::HDMIA));
|
||||
@@ -1744,10 +1582,7 @@ mod tests {
|
||||
fn presentation_clock_rejects_realtime_timestamps() {
|
||||
let mut clock = PresentationClock::default();
|
||||
|
||||
assert_eq!(
|
||||
clock.record(DrmEventTime::Realtime(SystemTime::now()), 1),
|
||||
None
|
||||
);
|
||||
assert_eq!(clock.record(DrmEventTime::Realtime(SystemTime::now()), 1), None);
|
||||
assert_eq!(clock.presented_frames, 0);
|
||||
}
|
||||
|
||||
@@ -1770,10 +1605,7 @@ mod tests {
|
||||
|
||||
clock.reset_timing();
|
||||
|
||||
assert_eq!(
|
||||
clock.record(DrmEventTime::Monotonic(Duration::from_secs(10)), 2),
|
||||
None
|
||||
);
|
||||
assert_eq!(clock.record(DrmEventTime::Monotonic(Duration::from_secs(10)), 2), None);
|
||||
assert_eq!(clock.presented_frames, 2);
|
||||
assert_eq!(clock.missed_deadlines, 0);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@ use std::ffi::OsString;
|
||||
|
||||
use smithay::reexports::calloop::EventLoop;
|
||||
|
||||
use crate::{Ferese, private_client::ClientCapabilities};
|
||||
use crate::Ferese;
|
||||
use crate::private_client::ClientCapabilities;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum BackendKind {
|
||||
@@ -21,16 +22,12 @@ pub struct LaunchConfig {
|
||||
|
||||
impl LaunchConfig {
|
||||
pub fn from_environment() -> Result<Self, String> {
|
||||
let graphical_session =
|
||||
std::env::var_os("WAYLAND_DISPLAY").is_some() || std::env::var_os("DISPLAY").is_some();
|
||||
let graphical_session = std::env::var_os("WAYLAND_DISPLAY").is_some() || std::env::var_os("DISPLAY").is_some();
|
||||
|
||||
Self::parse(std::env::args_os().skip(1), graphical_session)
|
||||
}
|
||||
|
||||
fn parse(
|
||||
args: impl IntoIterator<Item = OsString>,
|
||||
graphical_session: bool,
|
||||
) -> Result<Self, String> {
|
||||
fn parse(args: impl IntoIterator<Item = OsString>, graphical_session: bool) -> Result<Self, String> {
|
||||
let mut args = args.into_iter().peekable();
|
||||
let mut requested_backend = None;
|
||||
let mut client = Vec::new();
|
||||
@@ -99,10 +96,7 @@ fn parse_backend(value: &OsString) -> Result<Option<BackendKind>, String> {
|
||||
Some("auto") => Ok(None),
|
||||
Some("nested") => Ok(Some(BackendKind::Nested)),
|
||||
Some("drm") => Ok(Some(BackendKind::Drm)),
|
||||
_ => Err(format!(
|
||||
"unknown backend {:?}; expected auto, nested, or drm",
|
||||
value
|
||||
)),
|
||||
_ => Err(format!("unknown backend {:?}; expected auto, nested, or drm", value)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,42 +155,25 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(config.client, [OsString::from("ferese-effects-probe")]);
|
||||
assert!(
|
||||
config
|
||||
.client_capabilities
|
||||
.contains(ClientCapabilities::EFFECTS)
|
||||
);
|
||||
assert!(config.client_capabilities.contains(ClientCapabilities::EFFECTS));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicitly_grants_shell_control_to_the_launched_private_client() {
|
||||
let config = LaunchConfig::parse(
|
||||
[
|
||||
OsString::from("--grant-shell-control"),
|
||||
OsString::from("ferese-shell"),
|
||||
],
|
||||
[OsString::from("--grant-shell-control"), OsString::from("ferese-shell")],
|
||||
true,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(config.client, [OsString::from("ferese-shell")]);
|
||||
assert!(
|
||||
config
|
||||
.client_capabilities
|
||||
.contains(ClientCapabilities::SHELL_CONTROL)
|
||||
);
|
||||
assert!(
|
||||
!config
|
||||
.client_capabilities
|
||||
.contains(ClientCapabilities::EFFECTS)
|
||||
);
|
||||
assert!(config.client_capabilities.contains(ClientCapabilities::SHELL_CONTROL));
|
||||
assert!(!config.client_capabilities.contains(ClientCapabilities::EFFECTS));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_an_unknown_backend() {
|
||||
let error =
|
||||
LaunchConfig::parse([OsString::from("--backend"), OsString::from("other")], true)
|
||||
.unwrap_err();
|
||||
let error = LaunchConfig::parse([OsString::from("--backend"), OsString::from("other")], true).unwrap_err();
|
||||
|
||||
assert!(error.contains("unknown backend"));
|
||||
}
|
||||
|
||||
+113
-312
@@ -1,9 +1,7 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::error::Error;
|
||||
use std::fmt;
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
use std::{fmt, fs, io};
|
||||
|
||||
use ferese_animation::SpringConfig;
|
||||
use ferese_core::LayoutMode;
|
||||
@@ -11,7 +9,8 @@ use ferese_layout::{ColumnWidth, Direction, GapConfig, ViewportFocusStrategy};
|
||||
use serde::Deserialize;
|
||||
use smithay::input::keyboard::{Keycode, keysyms, xkb};
|
||||
|
||||
use crate::window_rules::{self, WindowRule, WindowRuleConfig};
|
||||
use crate::window_rules;
|
||||
use crate::window_rules::{WindowRule, WindowRuleConfig};
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
pub struct Config {
|
||||
@@ -163,11 +162,7 @@ impl BorderPaintConfig {
|
||||
"focus_ring.gradient.angle",
|
||||
)
|
||||
} else {
|
||||
(
|
||||
"border.gradient.from",
|
||||
"border.gradient.to",
|
||||
"border.gradient.angle",
|
||||
)
|
||||
("border.gradient.from", "border.gradient.to", "border.gradient.angle")
|
||||
};
|
||||
self.gradient
|
||||
.as_ref()
|
||||
@@ -274,18 +269,10 @@ impl Config {
|
||||
}
|
||||
|
||||
pub(crate) fn runtime_config(&self) -> Result<crate::RuntimeConfig, ConfigError> {
|
||||
self.notifications
|
||||
.validate()
|
||||
.map_err(ConfigError::InvalidBinding)?;
|
||||
self.desktop_widgets
|
||||
.validate()
|
||||
.map_err(ConfigError::InvalidBinding)?;
|
||||
self.notifications.validate().map_err(ConfigError::InvalidBinding)?;
|
||||
self.desktop_widgets.validate().map_err(ConfigError::InvalidBinding)?;
|
||||
for daemon in &self.autostart {
|
||||
if daemon
|
||||
.command
|
||||
.first()
|
||||
.is_none_or(|program| program.trim().is_empty())
|
||||
{
|
||||
if daemon.command.first().is_none_or(|program| program.trim().is_empty()) {
|
||||
return Err(ConfigError::InvalidBinding(
|
||||
"autostart command must contain a program".into(),
|
||||
));
|
||||
@@ -510,15 +497,10 @@ impl Binding {
|
||||
let symbol = parse_keysym(&key)?;
|
||||
if (modifiers.ctrl
|
||||
&& modifiers.alt
|
||||
&& matches!(
|
||||
symbol,
|
||||
keysyms::KEY_Escape | keysyms::KEY_F1..=keysyms::KEY_F12
|
||||
))
|
||||
&& matches!(symbol, keysyms::KEY_Escape | keysyms::KEY_F1..=keysyms::KEY_F12))
|
||||
|| (keysyms::KEY_XF86Switch_VT_1..=keysyms::KEY_XF86Switch_VT_12).contains(&symbol)
|
||||
{
|
||||
return Err(ConfigError::InvalidBinding(
|
||||
"Reserved system shortcut".into(),
|
||||
));
|
||||
return Err(ConfigError::InvalidBinding("Reserved system shortcut".into()));
|
||||
}
|
||||
Ok(Self {
|
||||
modifiers,
|
||||
@@ -534,34 +516,19 @@ impl Binding {
|
||||
match (&self.trigger, &other.trigger) {
|
||||
(BindingTrigger::Keysym(left), BindingTrigger::Keysym(right)) => left == right,
|
||||
(BindingTrigger::Keysym(symbol), BindingTrigger::Physical(code))
|
||||
| (BindingTrigger::Physical(code), BindingTrigger::Keysym(symbol)) => {
|
||||
(0..map.num_layouts_for_key(*code)).any(|layout| {
|
||||
| (BindingTrigger::Physical(code), BindingTrigger::Keysym(symbol)) => (0..map.num_layouts_for_key(*code))
|
||||
.any(|layout| {
|
||||
map.key_get_syms_by_level(*code, layout, 0)
|
||||
.iter()
|
||||
.any(|key| key.raw() == *symbol)
|
||||
})
|
||||
}
|
||||
}),
|
||||
(BindingTrigger::Physical(left), BindingTrigger::Physical(right)) => left == right,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn matches(
|
||||
&self,
|
||||
keycode: Keycode,
|
||||
keysyms: &[u32],
|
||||
logo: bool,
|
||||
ctrl: bool,
|
||||
alt: bool,
|
||||
shift: bool,
|
||||
) -> bool {
|
||||
self.modifiers
|
||||
== BindingModifiers {
|
||||
logo,
|
||||
ctrl,
|
||||
alt,
|
||||
shift,
|
||||
}
|
||||
pub fn matches(&self, keycode: Keycode, keysyms: &[u32], logo: bool, ctrl: bool, alt: bool, shift: bool) -> bool {
|
||||
self.modifiers == BindingModifiers { logo, ctrl, alt, shift }
|
||||
&& match self.trigger {
|
||||
BindingTrigger::Keysym(expected) => keysyms.contains(&expected),
|
||||
BindingTrigger::Physical(expected) => expected == keycode,
|
||||
@@ -572,11 +539,7 @@ impl Binding {
|
||||
pub(crate) fn guide_entry(&self, keymap: Option<&xkb::Keymap>) -> Option<serde_json::Value> {
|
||||
let description = match &self.action {
|
||||
BindingAction::None => return None,
|
||||
BindingAction::Spawn(argv)
|
||||
if argv
|
||||
.first()
|
||||
.is_some_and(|program| program == "ferese-screenshot") =>
|
||||
{
|
||||
BindingAction::Spawn(argv) if argv.first().is_some_and(|program| program == "ferese-screenshot") => {
|
||||
if argv.iter().any(|arg| arg == "--full") {
|
||||
"Capture whole screen"
|
||||
} else {
|
||||
@@ -598,12 +561,9 @@ impl Binding {
|
||||
BindingAction::Resize(direction) => format!("Resize window {direction:?}"),
|
||||
BindingAction::SwitchWorkspace(index) => format!("Go to workspace {index}"),
|
||||
BindingAction::MoveToWorkspace(index) => format!("Move window to workspace {index}"),
|
||||
BindingAction::SwitchRelativeWorkspace(next) => if *next {
|
||||
"Next workspace"
|
||||
} else {
|
||||
"Previous workspace"
|
||||
BindingAction::SwitchRelativeWorkspace(next) => {
|
||||
if *next { "Next workspace" } else { "Previous workspace" }.into()
|
||||
}
|
||||
.into(),
|
||||
BindingAction::ToggleFullscreen => "Toggle fullscreen".into(),
|
||||
BindingAction::ToggleMaximized => "Toggle full-width window".into(),
|
||||
BindingAction::ToggleLayout => "Switch workspace layout".into(),
|
||||
@@ -651,11 +611,7 @@ impl Binding {
|
||||
matches!(self.trigger, BindingTrigger::Swipe { fingers, .. } if fingers == count)
|
||||
}
|
||||
|
||||
pub(crate) fn matches_swipe(
|
||||
&self,
|
||||
count: u32,
|
||||
target: crate::gestures::SwipeDirection,
|
||||
) -> bool {
|
||||
pub(crate) fn matches_swipe(&self, count: u32, target: crate::gestures::SwipeDirection) -> bool {
|
||||
matches!(self.trigger, BindingTrigger::Swipe { fingers, direction } if fingers == count && direction == target)
|
||||
}
|
||||
}
|
||||
@@ -1022,11 +978,7 @@ impl Config {
|
||||
}
|
||||
|
||||
pub fn scrolling_focus_strategy(&self) -> ViewportFocusStrategy {
|
||||
match self
|
||||
.scrolling
|
||||
.focus_strategy
|
||||
.unwrap_or(FocusStrategyValue::Minimal)
|
||||
{
|
||||
match self.scrolling.focus_strategy.unwrap_or(FocusStrategyValue::Minimal) {
|
||||
FocusStrategyValue::Minimal => ViewportFocusStrategy::Minimal,
|
||||
FocusStrategyValue::CenterOnFocus => ViewportFocusStrategy::Center,
|
||||
FocusStrategyValue::Paged => ViewportFocusStrategy::Paged,
|
||||
@@ -1078,10 +1030,7 @@ impl Config {
|
||||
pub fn bindings(&self, input: &InputSettings) -> Result<Vec<Binding>, ConfigError> {
|
||||
let mut commands = HashMap::from([
|
||||
("terminal".to_owned(), vec!["foot".to_owned()]),
|
||||
(
|
||||
"screenshot".to_owned(),
|
||||
vec!["ferese-screenshot".to_owned()],
|
||||
),
|
||||
("screenshot".to_owned(), vec!["ferese-screenshot".to_owned()]),
|
||||
(
|
||||
"screenshot-full".to_owned(),
|
||||
vec!["ferese-screenshot".to_owned(), "--full".to_owned()],
|
||||
@@ -1197,19 +1146,13 @@ impl Config {
|
||||
}
|
||||
|
||||
pub fn theme_settings(&self) -> Result<ThemeSettings, ConfigError> {
|
||||
let border_width =
|
||||
nonnegative_theme_value(self.theme.geometry.border_width, "geometry.border_width")?;
|
||||
let focus_ring_width = nonnegative_theme_value(
|
||||
self.theme.geometry.focus_ring_width,
|
||||
"geometry.focus_ring_width",
|
||||
)?;
|
||||
let window_radius =
|
||||
nonnegative_theme_value(self.theme.geometry.window_radius, "geometry.window_radius")?;
|
||||
let shadow_offset_y =
|
||||
finite_theme_value(self.theme.shadow.soft.offset_y, "shadow.soft.offset_y")?;
|
||||
let border_width = nonnegative_theme_value(self.theme.geometry.border_width, "geometry.border_width")?;
|
||||
let focus_ring_width =
|
||||
nonnegative_theme_value(self.theme.geometry.focus_ring_width, "geometry.focus_ring_width")?;
|
||||
let window_radius = nonnegative_theme_value(self.theme.geometry.window_radius, "geometry.window_radius")?;
|
||||
let shadow_offset_y = finite_theme_value(self.theme.shadow.soft.offset_y, "shadow.soft.offset_y")?;
|
||||
let shadow_blur = nonnegative_theme_value(self.theme.shadow.soft.blur, "shadow.soft.blur")?;
|
||||
let shadow_opacity =
|
||||
unit_theme_value(self.theme.shadow.soft.opacity, "shadow.soft.opacity")?;
|
||||
let shadow_opacity = unit_theme_value(self.theme.shadow.soft.opacity, "shadow.soft.opacity")?;
|
||||
|
||||
let material_radius = nonnegative_theme_value(
|
||||
self.theme
|
||||
@@ -1224,25 +1167,16 @@ impl Config {
|
||||
border_width,
|
||||
focus_ring_width,
|
||||
border_color: parse_color(&self.theme.colors.border, "colors.border")?,
|
||||
text_primary_color: parse_color(
|
||||
&self.theme.colors.text_primary,
|
||||
"colors.text_primary",
|
||||
)?,
|
||||
text_primary_color: parse_color(&self.theme.colors.text_primary, "colors.text_primary")?,
|
||||
accent_color: parse_color(&self.theme.colors.accent, "colors.accent")?,
|
||||
border_gradient: self.theme.border.settings("border")?,
|
||||
focus_ring_gradient: self.theme.focus_ring.settings("focus_ring")?,
|
||||
shadow_color: parse_color(&self.theme.colors.shadow, "colors.shadow")?,
|
||||
surface_base_color: parse_color(
|
||||
&self.theme.colors.surface_base,
|
||||
"colors.surface_base",
|
||||
)?,
|
||||
surface_base_color: parse_color(&self.theme.colors.surface_base, "colors.surface_base")?,
|
||||
shell_opacity: unit_theme_value(self.theme.material.opacity, "material.opacity")?,
|
||||
inactive_dim: InactiveDimSettings {
|
||||
enabled: self.appearance.inactive_dim.enabled,
|
||||
amount: unit_theme_value(
|
||||
self.appearance.inactive_dim.amount,
|
||||
"appearance.inactive_dim.amount",
|
||||
)?,
|
||||
amount: unit_theme_value(self.appearance.inactive_dim.amount, "appearance.inactive_dim.amount")?,
|
||||
duration_ms: nonnegative_theme_value(
|
||||
self.appearance.inactive_dim.duration_ms,
|
||||
"appearance.inactive_dim.duration_ms",
|
||||
@@ -1253,11 +1187,7 @@ impl Config {
|
||||
shadow_blur,
|
||||
shadow_opacity,
|
||||
material_style: self.theme.material.style,
|
||||
backdrop_blur: nonnegative_theme_value(
|
||||
self.theme.material.blur_radius,
|
||||
"material.blur_radius",
|
||||
)?
|
||||
.min(32.0),
|
||||
backdrop_blur: nonnegative_theme_value(self.theme.material.blur_radius, "material.blur_radius")?.min(32.0),
|
||||
material_radius,
|
||||
panel_radius: material_radius,
|
||||
})
|
||||
@@ -1273,8 +1203,7 @@ impl Config {
|
||||
|
||||
pub fn spring_config(&self) -> Result<SpringConfig, ConfigError> {
|
||||
let mass = positive_animation_value(self.animations.spring.mass, "spring.mass")?;
|
||||
let stiffness =
|
||||
positive_animation_value(self.animations.spring.stiffness, "spring.stiffness")?;
|
||||
let stiffness = positive_animation_value(self.animations.spring.stiffness, "spring.stiffness")?;
|
||||
let damping = self.animations.spring.damping;
|
||||
if !damping.is_finite() || damping < 0.0 {
|
||||
return Err(ConfigError::InvalidAnimationValue {
|
||||
@@ -1292,12 +1221,9 @@ impl Config {
|
||||
}
|
||||
|
||||
pub fn viewport_spring_config(&self) -> Result<SpringConfig, ConfigError> {
|
||||
let mass =
|
||||
positive_animation_value(self.animations.viewport_spring.mass, "viewport_spring.mass")?;
|
||||
let stiffness = positive_animation_value(
|
||||
self.animations.viewport_spring.stiffness,
|
||||
"viewport_spring.stiffness",
|
||||
)?;
|
||||
let mass = positive_animation_value(self.animations.viewport_spring.mass, "viewport_spring.mass")?;
|
||||
let stiffness =
|
||||
positive_animation_value(self.animations.viewport_spring.stiffness, "viewport_spring.stiffness")?;
|
||||
let damping_ratio = positive_animation_value(
|
||||
self.animations.viewport_spring.damping_ratio,
|
||||
"viewport_spring.damping_ratio",
|
||||
@@ -1322,9 +1248,7 @@ struct BindingIdentity {
|
||||
fn validate_commands(commands: &HashMap<String, Vec<String>>) -> Result<(), ConfigError> {
|
||||
for (name, argv) in commands {
|
||||
if name.trim().is_empty() {
|
||||
return Err(ConfigError::InvalidBinding(
|
||||
"command names cannot be empty".to_owned(),
|
||||
));
|
||||
return Err(ConfigError::InvalidBinding("command names cannot be empty".to_owned()));
|
||||
}
|
||||
if argv.is_empty() || argv[0].is_empty() {
|
||||
return Err(ConfigError::InvalidBinding(format!(
|
||||
@@ -1365,9 +1289,10 @@ fn parse_binding(
|
||||
}
|
||||
|
||||
let identity = binding_identity(configured, keymap)?;
|
||||
let action = configured.action.as_deref().ok_or_else(|| {
|
||||
ConfigError::InvalidBinding(format!("binding {:?} has no action", configured.keys))
|
||||
})?;
|
||||
let action = configured
|
||||
.action
|
||||
.as_deref()
|
||||
.ok_or_else(|| ConfigError::InvalidBinding(format!("binding {:?} has no action", configured.keys)))?;
|
||||
let action = parse_action(action, configured.argument.as_deref(), commands)?;
|
||||
let trigger = match identity.match_mode {
|
||||
BindingMatch::Keysym => BindingTrigger::Keysym(identity.key),
|
||||
@@ -1390,15 +1315,10 @@ fn parse_binding(
|
||||
})
|
||||
}
|
||||
|
||||
fn binding_identity(
|
||||
configured: &BindingConfig,
|
||||
keymap: &xkb::Keymap,
|
||||
) -> Result<BindingIdentity, ConfigError> {
|
||||
fn binding_identity(configured: &BindingConfig, keymap: &xkb::Keymap) -> Result<BindingIdentity, ConfigError> {
|
||||
let (modifiers, key) = parse_chord(&configured.keys)?;
|
||||
if let Some((fingers, direction)) = parse_swipe(&key) {
|
||||
if configured.match_mode == BindingMatch::Physical
|
||||
|| modifiers != BindingModifiers::default()
|
||||
{
|
||||
if configured.match_mode == BindingMatch::Physical || modifiers != BindingModifiers::default() {
|
||||
return Err(ConfigError::InvalidBinding(
|
||||
"swipes cannot use keyboard modifiers or physical key matching".into(),
|
||||
));
|
||||
@@ -1438,9 +1358,7 @@ fn binding_identity_for_runtime(binding: &Binding) -> BindingIdentity {
|
||||
let (match_mode, key) = match binding.trigger {
|
||||
BindingTrigger::Keysym(key) => (BindingMatch::Keysym, key),
|
||||
BindingTrigger::Physical(key) => (BindingMatch::Physical, key.raw()),
|
||||
BindingTrigger::Swipe { fingers, direction } => {
|
||||
(BindingMatch::Swipe, fingers * 4 + direction as u32)
|
||||
}
|
||||
BindingTrigger::Swipe { fingers, direction } => (BindingMatch::Swipe, fingers * 4 + direction as u32),
|
||||
};
|
||||
|
||||
BindingIdentity {
|
||||
@@ -1456,9 +1374,7 @@ fn parse_chord(chord: &str) -> Result<(BindingModifiers, String), ConfigError> {
|
||||
|
||||
for component in chord.split('+').map(str::trim) {
|
||||
if component.is_empty() {
|
||||
return Err(ConfigError::InvalidBinding(format!(
|
||||
"invalid key chord {chord:?}"
|
||||
)));
|
||||
return Err(ConfigError::InvalidBinding(format!("invalid key chord {chord:?}")));
|
||||
}
|
||||
|
||||
let slot = match component.to_ascii_lowercase().as_str() {
|
||||
@@ -1470,9 +1386,7 @@ fn parse_chord(chord: &str) -> Result<(BindingModifiers, String), ConfigError> {
|
||||
};
|
||||
if let Some(slot) = slot {
|
||||
if *slot {
|
||||
return Err(ConfigError::InvalidBinding(format!(
|
||||
"duplicate modifier in {chord:?}"
|
||||
)));
|
||||
return Err(ConfigError::InvalidBinding(format!("duplicate modifier in {chord:?}")));
|
||||
}
|
||||
*slot = true;
|
||||
} else if key.replace(component.to_owned()).is_some() {
|
||||
@@ -1482,9 +1396,7 @@ fn parse_chord(chord: &str) -> Result<(BindingModifiers, String), ConfigError> {
|
||||
}
|
||||
}
|
||||
|
||||
let key = key.ok_or_else(|| {
|
||||
ConfigError::InvalidBinding(format!("key chord {chord:?} does not contain a key"))
|
||||
})?;
|
||||
let key = key.ok_or_else(|| ConfigError::InvalidBinding(format!("key chord {chord:?} does not contain a key")))?;
|
||||
Ok((modifiers, key))
|
||||
}
|
||||
|
||||
@@ -1520,9 +1432,7 @@ fn parse_keysym(name: &str) -> Result<u32, ConfigError> {
|
||||
symbol = xkb::keysym_from_name(&normalized, xkb::KEYSYM_CASE_INSENSITIVE);
|
||||
}
|
||||
if symbol.raw() == keysyms::KEY_NoSymbol {
|
||||
Err(ConfigError::InvalidBinding(format!(
|
||||
"unknown keysym {name:?}"
|
||||
)))
|
||||
Err(ConfigError::InvalidBinding(format!("unknown keysym {name:?}")))
|
||||
} else {
|
||||
Ok(symbol.raw())
|
||||
}
|
||||
@@ -1542,11 +1452,8 @@ fn parse_action(
|
||||
Ok(())
|
||||
}
|
||||
};
|
||||
let required_argument = || {
|
||||
argument.ok_or_else(|| {
|
||||
ConfigError::InvalidBinding(format!("action {action:?} requires an argument"))
|
||||
})
|
||||
};
|
||||
let required_argument =
|
||||
|| argument.ok_or_else(|| ConfigError::InvalidBinding(format!("action {action:?} requires an argument")));
|
||||
|
||||
match action {
|
||||
"none" => {
|
||||
@@ -1555,15 +1462,13 @@ fn parse_action(
|
||||
}
|
||||
"workspace-next" | "workspace-previous" => {
|
||||
no_argument()?;
|
||||
Ok(BindingAction::SwitchRelativeWorkspace(
|
||||
action == "workspace-next",
|
||||
))
|
||||
Ok(BindingAction::SwitchRelativeWorkspace(action == "workspace-next"))
|
||||
}
|
||||
"spawn" => {
|
||||
let command = required_argument()?;
|
||||
let argv = commands.get(command).ok_or_else(|| {
|
||||
ConfigError::InvalidBinding(format!("unknown command {command:?}"))
|
||||
})?;
|
||||
let argv = commands
|
||||
.get(command)
|
||||
.ok_or_else(|| ConfigError::InvalidBinding(format!("unknown command {command:?}")))?;
|
||||
Ok(BindingAction::Spawn(argv.clone()))
|
||||
}
|
||||
"close" => {
|
||||
@@ -1576,15 +1481,9 @@ fn parse_action(
|
||||
}
|
||||
"focus" => Ok(BindingAction::Focus(parse_direction(required_argument()?)?)),
|
||||
"move" => Ok(BindingAction::Move(parse_direction(required_argument()?)?)),
|
||||
"resize" => Ok(BindingAction::Resize(
|
||||
parse_direction(required_argument()?)?,
|
||||
)),
|
||||
"workspace" => Ok(BindingAction::SwitchWorkspace(parse_workspace(
|
||||
required_argument()?,
|
||||
)?)),
|
||||
"move-to-workspace" => Ok(BindingAction::MoveToWorkspace(parse_workspace(
|
||||
required_argument()?,
|
||||
)?)),
|
||||
"resize" => Ok(BindingAction::Resize(parse_direction(required_argument()?)?)),
|
||||
"workspace" => Ok(BindingAction::SwitchWorkspace(parse_workspace(required_argument()?)?)),
|
||||
"move-to-workspace" => Ok(BindingAction::MoveToWorkspace(parse_workspace(required_argument()?)?)),
|
||||
"toggle-maximized" => {
|
||||
no_argument()?;
|
||||
Ok(BindingAction::ToggleMaximized)
|
||||
@@ -1621,9 +1520,7 @@ fn parse_action(
|
||||
no_argument()?;
|
||||
Ok(BindingAction::ToggleOverview)
|
||||
}
|
||||
_ => Err(ConfigError::InvalidBinding(format!(
|
||||
"unknown action {action:?}"
|
||||
))),
|
||||
_ => Err(ConfigError::InvalidBinding(format!("unknown action {action:?}"))),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1633,18 +1530,14 @@ fn parse_direction(argument: &str) -> Result<Direction, ConfigError> {
|
||||
"right" => Ok(Direction::Right),
|
||||
"up" => Ok(Direction::Up),
|
||||
"down" => Ok(Direction::Down),
|
||||
_ => Err(ConfigError::InvalidBinding(format!(
|
||||
"invalid direction {argument:?}"
|
||||
))),
|
||||
_ => Err(ConfigError::InvalidBinding(format!("invalid direction {argument:?}"))),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_workspace(argument: &str) -> Result<u8, ConfigError> {
|
||||
match argument.parse() {
|
||||
Ok(workspace) if workspace > 0 => Ok(workspace),
|
||||
_ => Err(ConfigError::InvalidBinding(format!(
|
||||
"invalid workspace {argument:?}"
|
||||
))),
|
||||
_ => Err(ConfigError::InvalidBinding(format!("invalid workspace {argument:?}"))),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1672,25 +1565,13 @@ fn default_bindings() -> Vec<BindingConfig> {
|
||||
|
||||
for (key, direction) in [("H", "left"), ("J", "down"), ("K", "up"), ("L", "right")] {
|
||||
bindings.push(binding("Super+".to_owned() + key, "focus", Some(direction)));
|
||||
bindings.push(binding(
|
||||
"Super+Shift+".to_owned() + key,
|
||||
"move",
|
||||
Some(direction),
|
||||
));
|
||||
bindings.push(binding(
|
||||
"Super+Ctrl+".to_owned() + key,
|
||||
"resize",
|
||||
Some(direction),
|
||||
));
|
||||
bindings.push(binding("Super+Shift+".to_owned() + key, "move", Some(direction)));
|
||||
bindings.push(binding("Super+Ctrl+".to_owned() + key, "resize", Some(direction)));
|
||||
}
|
||||
|
||||
for workspace in 1..=9 {
|
||||
let workspace = workspace.to_string();
|
||||
bindings.push(binding(
|
||||
format!("Super+{workspace}"),
|
||||
"workspace",
|
||||
Some(&workspace),
|
||||
));
|
||||
bindings.push(binding(format!("Super+{workspace}"), "workspace", Some(&workspace)));
|
||||
bindings.push(binding(
|
||||
format!("Super+Shift+{workspace}"),
|
||||
"move-to-workspace",
|
||||
@@ -1701,11 +1582,7 @@ fn default_bindings() -> Vec<BindingConfig> {
|
||||
bindings
|
||||
}
|
||||
|
||||
fn binding(
|
||||
keys: impl Into<String>,
|
||||
action: impl Into<String>,
|
||||
argument: Option<&str>,
|
||||
) -> BindingConfig {
|
||||
fn binding(keys: impl Into<String>, action: impl Into<String>, argument: Option<&str>) -> BindingConfig {
|
||||
BindingConfig {
|
||||
keys: keys.into(),
|
||||
match_mode: BindingMatch::Keysym,
|
||||
@@ -1909,9 +1786,7 @@ fn parse_output_mode(value: &str) -> Result<OutputModeRequest, String> {
|
||||
let (size, refresh) = value
|
||||
.trim()
|
||||
.split_once('@')
|
||||
.map_or((value.trim(), None), |(size, refresh)| {
|
||||
(size, Some(refresh))
|
||||
});
|
||||
.map_or((value.trim(), None), |(size, refresh)| (size, Some(refresh)));
|
||||
let (width, height) = size
|
||||
.split_once('x')
|
||||
.ok_or_else(|| format!("mode {value:?} must use WIDTHxHEIGHT or WIDTHxHEIGHT@REFRESH"))?;
|
||||
@@ -1944,17 +1819,10 @@ fn parse_output_mode(value: &str) -> Result<OutputModeRequest, String> {
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_column_width(
|
||||
value: &ColumnWidthValue,
|
||||
field: &'static str,
|
||||
) -> Result<ColumnWidth, ConfigError> {
|
||||
fn parse_column_width(value: &ColumnWidthValue, field: &'static str) -> Result<ColumnWidth, ConfigError> {
|
||||
match value {
|
||||
ColumnWidthValue::Proportion(value) if value.is_finite() && *value > 0.0 => {
|
||||
Ok(ColumnWidth::Proportion(*value))
|
||||
}
|
||||
ColumnWidthValue::Named(value) if value.eq_ignore_ascii_case("full") => {
|
||||
Ok(ColumnWidth::Full)
|
||||
}
|
||||
ColumnWidthValue::Proportion(value) if value.is_finite() && *value > 0.0 => Ok(ColumnWidth::Proportion(*value)),
|
||||
ColumnWidthValue::Named(value) if value.eq_ignore_ascii_case("full") => Ok(ColumnWidth::Full),
|
||||
value => Err(ConfigError::InvalidColumnWidth {
|
||||
field,
|
||||
value: match value {
|
||||
@@ -1982,15 +1850,9 @@ mod tests {
|
||||
let source = "input {\n touchpad {\n swipe-threshold 96\n }\n}\nbinding keys=\"Swipe3Up\" action=\"toggle-overview\"\noutput-profile name=\"desk\" {\n output match=\"DP-1\" scale=1.5 {\n position 0 0\n }\n}\ndesktop-widgets {\n clock {\n enabled #true\n outputs \"DP-1\"\n }\n}\nautostart {\n command \"program\" \"argument with space\"\n}\n";
|
||||
let config = Config::parse_source(source).unwrap();
|
||||
config.runtime_config().unwrap();
|
||||
assert_eq!(
|
||||
config.input_settings().unwrap().touchpad.swipe_threshold,
|
||||
96
|
||||
);
|
||||
assert_eq!(config.input_settings().unwrap().touchpad.swipe_threshold, 96);
|
||||
assert_eq!(config.desktop_widgets.clock.outputs, ["DP-1"]);
|
||||
assert_eq!(
|
||||
config.output_profiles().unwrap()[0].outputs[0].position,
|
||||
Some([0, 0])
|
||||
);
|
||||
assert_eq!(config.output_profiles().unwrap()[0].outputs[0].position, Some([0, 0]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2026,10 +1888,7 @@ mod tests {
|
||||
let config = parse("");
|
||||
|
||||
assert_eq!(config.layout_mode(), LayoutMode::Scrolling);
|
||||
assert_eq!(
|
||||
config.default_column_width().unwrap(),
|
||||
ColumnWidth::Proportion(0.5)
|
||||
);
|
||||
assert_eq!(config.default_column_width().unwrap(), ColumnWidth::Proportion(0.5));
|
||||
assert_eq!(config.gap_config().unwrap(), GapConfig::default());
|
||||
}
|
||||
|
||||
@@ -2039,10 +1898,7 @@ mod tests {
|
||||
let numeric = parse("scrolling {\n default-column-width 1.0\n}\n");
|
||||
|
||||
assert_eq!(full.default_column_width().unwrap(), ColumnWidth::Full);
|
||||
assert_eq!(
|
||||
numeric.default_column_width().unwrap(),
|
||||
ColumnWidth::Proportion(1.0)
|
||||
);
|
||||
assert_eq!(numeric.default_column_width().unwrap(), ColumnWidth::Proportion(1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2066,19 +1922,10 @@ mod tests {
|
||||
let minimal = parse("");
|
||||
let centered = parse("scrolling {\n focus-strategy \"center_on_focus\"\n}\n");
|
||||
let paged = parse("scrolling {\n focus-strategy \"paged\"\n}\n");
|
||||
assert_eq!(
|
||||
paged.scrolling_focus_strategy(),
|
||||
ViewportFocusStrategy::Paged
|
||||
);
|
||||
assert_eq!(paged.scrolling_focus_strategy(), ViewportFocusStrategy::Paged);
|
||||
|
||||
assert_eq!(
|
||||
minimal.scrolling_focus_strategy(),
|
||||
ViewportFocusStrategy::Minimal
|
||||
);
|
||||
assert_eq!(
|
||||
centered.scrolling_focus_strategy(),
|
||||
ViewportFocusStrategy::Center
|
||||
);
|
||||
assert_eq!(minimal.scrolling_focus_strategy(), ViewportFocusStrategy::Minimal);
|
||||
assert_eq!(centered.scrolling_focus_strategy(), ViewportFocusStrategy::Center);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2120,8 +1967,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn parses_and_validates_layout_gaps() {
|
||||
let configured =
|
||||
parse("layout {\n inner-gap 6.0\n outer-gap 14.0\n smart-gaps #true\n}\n");
|
||||
let configured = parse("layout {\n inner-gap 6.0\n outer-gap 14.0\n smart-gaps #true\n}\n");
|
||||
let invalid = parse("layout {\n outer-gap -1.0\n}\n");
|
||||
|
||||
assert_eq!(
|
||||
@@ -2159,11 +2005,9 @@ mod tests {
|
||||
"theme {\n colors {\n border \"#11223344\"\n accent \"#AABBCC\"\n shadow \"#01020380\"\n }\n geometry {\n border-width 1.5\n focus-ring-width 3.0\n window-radius 12.0\n }\n shadow {\n soft {\n offset-y -2.0\n blur 24.0\n opacity 0.4\n }\n }\n material {\n style \"translucent\"\n }\n}\n",
|
||||
);
|
||||
let invalid_color = parse("theme {\n colors {\n accent \"blue\"\n }\n}\n");
|
||||
let invalid_radius =
|
||||
parse("theme {\n geometry {\n window-radius -1.0\n }\n}\n");
|
||||
let invalid_opacity = parse(
|
||||
"theme {\n shadow {\n soft {\n opacity 1.1\n }\n }\n}\n",
|
||||
);
|
||||
let invalid_radius = parse("theme {\n geometry {\n window-radius -1.0\n }\n}\n");
|
||||
let invalid_opacity =
|
||||
parse("theme {\n shadow {\n soft {\n opacity 1.1\n }\n }\n}\n");
|
||||
|
||||
assert_eq!(
|
||||
configured.theme_settings().unwrap(),
|
||||
@@ -2197,24 +2041,15 @@ mod tests {
|
||||
assert!(invalid_radius.theme_settings().is_err());
|
||||
assert!(invalid_opacity.theme_settings().is_err());
|
||||
assert!(
|
||||
ferese_config::from_str::<Config>(
|
||||
"theme {\n material {\n style \"mist\"\n }\n}\n"
|
||||
)
|
||||
.is_err()
|
||||
ferese_config::from_str::<Config>("theme {\n material {\n style \"mist\"\n }\n}\n").is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn materials_default_to_solid_and_reject_removed_style() {
|
||||
assert_eq!(
|
||||
parse("").theme_settings().unwrap().material_style,
|
||||
MaterialStyle::Solid
|
||||
);
|
||||
assert_eq!(parse("").theme_settings().unwrap().material_style, MaterialStyle::Solid);
|
||||
assert!(
|
||||
ferese_config::from_str::<Config>(
|
||||
"theme {\n material {\n style \"glass\"\n }\n}\n"
|
||||
)
|
||||
.is_err()
|
||||
ferese_config::from_str::<Config>("theme {\n material {\n style \"glass\"\n }\n}\n").is_err()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2249,14 +2084,17 @@ mod tests {
|
||||
"from \"#112233\"\nto \"#445566\"\nangle #inf\n",
|
||||
] {
|
||||
assert!(
|
||||
Config::parse_source(&format!(
|
||||
"theme {{ focus-ring {{ gradient {{\n{settings}\n}} }} }}"
|
||||
))
|
||||
.and_then(|config| config.theme_settings())
|
||||
.is_err()
|
||||
Config::parse_source(&format!("theme {{ focus-ring {{ gradient {{\n{settings}\n}} }} }}"))
|
||||
.and_then(|config| config.theme_settings())
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
assert!(ferese_config::from_str::<Config>("theme {\n border {\n gradient {\n from \"#112233\"\n }\n }\n}\n").is_err());
|
||||
assert!(
|
||||
ferese_config::from_str::<Config>(
|
||||
"theme {\n border {\n gradient {\n from \"#112233\"\n }\n }\n}\n"
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2336,11 +2174,9 @@ mod tests {
|
||||
("duration_ms", "#inf"),
|
||||
] {
|
||||
assert!(
|
||||
Config::parse_source(&format!(
|
||||
"appearance {{ inactive-dim {{ {key} {value}; }} }}"
|
||||
))
|
||||
.and_then(|config| config.theme_settings())
|
||||
.is_err()
|
||||
Config::parse_source(&format!("appearance {{ inactive-dim {{ {key} {value}; }} }}"))
|
||||
.and_then(|config| config.theme_settings())
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2409,11 +2245,9 @@ mod tests {
|
||||
);
|
||||
for threshold in [0, 15, 1001] {
|
||||
assert!(
|
||||
parse(&format!(
|
||||
"input {{ touchpad {{ swipe-threshold {threshold}; }} }}"
|
||||
))
|
||||
.input_settings()
|
||||
.is_err()
|
||||
parse(&format!("input {{ touchpad {{ swipe-threshold {threshold}; }} }}"))
|
||||
.input_settings()
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2435,9 +2269,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn guide_uses_effective_bindings_including_overrides_and_unbindings() {
|
||||
let config = parse(
|
||||
"binding keys=\"Super+Q\" action=\"none\"\nbinding keys=\"Super+Tab\" action=\"toggle-floating\"\n",
|
||||
);
|
||||
let config =
|
||||
parse("binding keys=\"Super+Q\" action=\"none\"\nbinding keys=\"Super+Tab\" action=\"toggle-floating\"\n");
|
||||
let input = config.input_settings().unwrap();
|
||||
let map = physical_keymap(&input).unwrap();
|
||||
let entries = config
|
||||
@@ -2446,13 +2279,12 @@ mod tests {
|
||||
.iter()
|
||||
.filter_map(|binding| binding.guide_entry(Some(&map)))
|
||||
.collect::<Vec<_>>();
|
||||
assert!(!entries.iter().any(|entry| entry["description"] == "Close window"));
|
||||
assert!(
|
||||
!entries
|
||||
entries
|
||||
.iter()
|
||||
.any(|entry| entry["description"] == "Close window")
|
||||
.any(|entry| entry["keys"] == "Super + Tab" && entry["description"] == "Toggle floating window")
|
||||
);
|
||||
assert!(entries.iter().any(|entry| entry["keys"] == "Super + Tab"
|
||||
&& entry["description"] == "Toggle floating window"));
|
||||
assert!(entries.iter().any(|entry| entry["keys"] == "Super + Enter"));
|
||||
}
|
||||
|
||||
@@ -2472,11 +2304,7 @@ mod tests {
|
||||
&& binding.trigger == BindingTrigger::Keysym(keysyms::KEY_f)
|
||||
&& binding.action == action));
|
||||
}
|
||||
assert!(
|
||||
bindings
|
||||
.iter()
|
||||
.any(|binding| binding.action == BindingAction::Exit)
|
||||
);
|
||||
assert!(bindings.iter().any(|binding| binding.action == BindingAction::Exit));
|
||||
assert!(bindings.iter().any(|binding| {
|
||||
binding.modifiers.logo
|
||||
&& binding.trigger == BindingTrigger::Keysym(keysyms::KEY_Return)
|
||||
@@ -2491,11 +2319,7 @@ mod tests {
|
||||
assert!(bindings.iter().any(|binding| {
|
||||
binding.trigger == BindingTrigger::Keysym(keysyms::KEY_Print)
|
||||
&& binding.modifiers == BindingModifiers::default()
|
||||
&& binding.action
|
||||
== BindingAction::Spawn(vec![
|
||||
"ferese-screenshot".to_owned(),
|
||||
"--full".to_owned(),
|
||||
])
|
||||
&& binding.action == BindingAction::Spawn(vec!["ferese-screenshot".to_owned(), "--full".to_owned()])
|
||||
}));
|
||||
assert!(bindings.iter().any(|binding| {
|
||||
binding.modifiers.logo
|
||||
@@ -2515,29 +2339,18 @@ mod tests {
|
||||
let bindings = replaced.bindings(&input).unwrap();
|
||||
assert_eq!(bindings.len(), 48);
|
||||
assert!(bindings.iter().any(|binding| {
|
||||
binding.action
|
||||
== BindingAction::Spawn(vec![
|
||||
"foot".to_owned(),
|
||||
"--app-id".to_owned(),
|
||||
"work".to_owned(),
|
||||
])
|
||||
binding.action == BindingAction::Spawn(vec!["foot".to_owned(), "--app-id".to_owned(), "work".to_owned()])
|
||||
}));
|
||||
|
||||
let input = unbound.input_settings().unwrap();
|
||||
let bindings = unbound.bindings(&input).unwrap();
|
||||
assert_eq!(bindings.len(), 47);
|
||||
assert!(
|
||||
!bindings
|
||||
.iter()
|
||||
.any(|binding| binding.action == BindingAction::Close)
|
||||
);
|
||||
assert!(!bindings.iter().any(|binding| binding.action == BindingAction::Close));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accepts_physical_xkb_key_names() {
|
||||
let config = parse(
|
||||
"binding keys=\"Super+AD06\" match=\"physical\" action=\"focus\" argument=\"left\"\n",
|
||||
);
|
||||
let config = parse("binding keys=\"Super+AD06\" match=\"physical\" action=\"focus\" argument=\"left\"\n");
|
||||
let input = config.input_settings().unwrap();
|
||||
let bindings = config.bindings(&input).unwrap();
|
||||
|
||||
@@ -2549,13 +2362,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn rejects_duplicate_or_invalid_user_bindings() {
|
||||
let duplicate = parse(
|
||||
"binding keys=\"Super+Q\" action=\"close\"\nbinding keys=\"logo+q\" action=\"close\"\n",
|
||||
);
|
||||
let missing_command =
|
||||
parse("binding keys=\"Super+Enter\" action=\"spawn\" argument=\"missing\"\n");
|
||||
let invalid_argument =
|
||||
parse("binding keys=\"Super+Q\" action=\"close\" argument=\"left\"\n");
|
||||
let duplicate = parse("binding keys=\"Super+Q\" action=\"close\"\nbinding keys=\"logo+q\" action=\"close\"\n");
|
||||
let missing_command = parse("binding keys=\"Super+Enter\" action=\"spawn\" argument=\"missing\"\n");
|
||||
let invalid_argument = parse("binding keys=\"Super+Q\" action=\"close\" argument=\"left\"\n");
|
||||
|
||||
for config in [duplicate, missing_command, invalid_argument] {
|
||||
let input = config.input_settings().unwrap();
|
||||
@@ -2569,12 +2378,7 @@ mod tests {
|
||||
"window-rule app-id=\"org.example.Editor\" workspace=3 floating=#true width=900.0 height=600.0 fullscreen=#false\n",
|
||||
);
|
||||
let rules = config.window_rules().unwrap();
|
||||
let result = window_rules::resolve(
|
||||
&rules,
|
||||
Some("org.example.editor.desktop"),
|
||||
Some("Document"),
|
||||
false,
|
||||
);
|
||||
let result = window_rules::resolve(&rules, Some("org.example.editor.desktop"), Some("Document"), false);
|
||||
|
||||
assert_eq!(result.workspace, Some(3));
|
||||
assert_eq!(result.floating, Some(true));
|
||||
@@ -2640,13 +2444,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn rejects_invalid_output_profiles() {
|
||||
let invalid_scale =
|
||||
parse("output-profile name=\"bad\" {\n output match=\"eDP-1\" scale=0.0\n}\n");
|
||||
let invalid_mode =
|
||||
parse("output-profile name=\"bad\" {\n output match=\"eDP-1\" mode=\"native\"\n}\n");
|
||||
let duplicate = parse(
|
||||
"output-profile name=\"bad\" {\n output match=\"eDP-1\"\n output match=\"eDP-1\"\n}\n",
|
||||
);
|
||||
let invalid_scale = parse("output-profile name=\"bad\" {\n output match=\"eDP-1\" scale=0.0\n}\n");
|
||||
let invalid_mode = parse("output-profile name=\"bad\" {\n output match=\"eDP-1\" mode=\"native\"\n}\n");
|
||||
let duplicate =
|
||||
parse("output-profile name=\"bad\" {\n output match=\"eDP-1\"\n output match=\"eDP-1\"\n}\n");
|
||||
|
||||
assert!(invalid_scale.output_profiles().is_err());
|
||||
assert!(invalid_mode.output_profiles().is_err());
|
||||
|
||||
+18
-35
@@ -1,11 +1,12 @@
|
||||
use std::{fs, sync::OnceLock};
|
||||
use std::fs;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use smithay::{
|
||||
backend::{allocator::Fourcc, renderer::element::memory::MemoryRenderBuffer},
|
||||
input::pointer::CursorIcon,
|
||||
utils::{Buffer, Point, Size, Transform},
|
||||
};
|
||||
use xcursor::{CursorTheme, parser::Image};
|
||||
use smithay::backend::allocator::Fourcc;
|
||||
use smithay::backend::renderer::element::memory::MemoryRenderBuffer;
|
||||
use smithay::input::pointer::CursorIcon;
|
||||
use smithay::utils::{Buffer, Point, Size, Transform};
|
||||
use xcursor::CursorTheme;
|
||||
use xcursor::parser::Image;
|
||||
|
||||
const DEFAULT_CURSOR_SIZE: u32 = 24;
|
||||
|
||||
@@ -58,26 +59,17 @@ fn load_image(theme: &CursorTheme, name: &str, requested_size: u32) -> Option<Im
|
||||
let path = theme.load_icon(name)?;
|
||||
let bytes = fs::read(path).ok()?;
|
||||
|
||||
xcursor::parser::parse_xcursor(&bytes)?
|
||||
.into_iter()
|
||||
.min_by_key(|image| {
|
||||
(
|
||||
image.size.abs_diff(requested_size),
|
||||
image.width.abs_diff(requested_size),
|
||||
)
|
||||
})
|
||||
xcursor::parser::parse_xcursor(&bytes)?.into_iter().min_by_key(|image| {
|
||||
(
|
||||
image.size.abs_diff(requested_size),
|
||||
image.width.abs_diff(requested_size),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn cursor_from_image(image: Image) -> NamedCursor {
|
||||
let size = Size::<i32, Buffer>::from((image.width as i32, image.height as i32));
|
||||
let buffer = MemoryRenderBuffer::from_slice(
|
||||
&image.pixels_rgba,
|
||||
Fourcc::Abgr8888,
|
||||
size,
|
||||
1,
|
||||
Transform::Normal,
|
||||
None,
|
||||
);
|
||||
let buffer = MemoryRenderBuffer::from_slice(&image.pixels_rgba, Fourcc::Abgr8888, size, 1, Transform::Normal, None);
|
||||
|
||||
NamedCursor {
|
||||
buffer,
|
||||
@@ -122,18 +114,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn supplies_legacy_fallback_names_for_common_cursors() {
|
||||
assert_eq!(
|
||||
cursor_names(CursorIcon::Default),
|
||||
["default", "left_ptr", "arrow"]
|
||||
);
|
||||
assert_eq!(
|
||||
cursor_names(CursorIcon::Pointer),
|
||||
["pointer", "hand2", "left_ptr"]
|
||||
);
|
||||
assert_eq!(
|
||||
cursor_names(CursorIcon::Text),
|
||||
["text", "xterm", "left_ptr"]
|
||||
);
|
||||
assert_eq!(cursor_names(CursorIcon::Default), ["default", "left_ptr", "arrow"]);
|
||||
assert_eq!(cursor_names(CursorIcon::Pointer), ["pointer", "hand2", "left_ptr"]);
|
||||
assert_eq!(cursor_names(CursorIcon::Text), ["text", "xterm", "left_ptr"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+10
-24
@@ -1,9 +1,10 @@
|
||||
//! Session-owned services. Never grant shell/effects privileges to daemons.
|
||||
use crate::{Ferese, config::DaemonConfig, private_client::ClientCapabilities};
|
||||
use std::{
|
||||
process::Child,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use std::process::Child;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use crate::Ferese;
|
||||
use crate::config::DaemonConfig;
|
||||
use crate::private_client::ClientCapabilities;
|
||||
|
||||
struct Service {
|
||||
config: DaemonConfig,
|
||||
@@ -17,15 +18,8 @@ pub(crate) struct Runner(Vec<Service>);
|
||||
impl Runner {
|
||||
pub fn reconcile(&mut self, configs: &[DaemonConfig], nested: bool) {
|
||||
let mut previous = std::mem::take(&mut self.0);
|
||||
for config in configs
|
||||
.iter()
|
||||
.filter(|c| c.enabled && (!nested || c.nested))
|
||||
{
|
||||
if self
|
||||
.0
|
||||
.iter()
|
||||
.any(|service| service.config.command == config.command)
|
||||
{
|
||||
for config in configs.iter().filter(|c| c.enabled && (!nested || c.nested)) {
|
||||
if self.0.iter().any(|service| service.config.command == config.command) {
|
||||
continue;
|
||||
}
|
||||
if let Some(index) = previous
|
||||
@@ -132,12 +126,7 @@ mod tests {
|
||||
nested: false,
|
||||
};
|
||||
let mut runner = Runner::new(&[config.clone()], false);
|
||||
runner.0[0].child = Some(
|
||||
std::process::Command::new("/bin/sleep")
|
||||
.arg("30")
|
||||
.spawn()
|
||||
.unwrap(),
|
||||
);
|
||||
runner.0[0].child = Some(std::process::Command::new("/bin/sleep").arg("30").spawn().unwrap());
|
||||
let pid = runner.0[0].child.as_ref().unwrap().id();
|
||||
config.restart = true;
|
||||
runner.reconcile(&[config.clone()], false);
|
||||
@@ -150,10 +139,7 @@ mod tests {
|
||||
assert!(runner.0.is_empty());
|
||||
let deadline = Instant::now() + Duration::from_secs(2);
|
||||
while unsafe { libc::kill(pid as i32, 0) } == 0 {
|
||||
assert!(
|
||||
Instant::now() < deadline,
|
||||
"removed service was not stopped/reaped"
|
||||
);
|
||||
assert!(Instant::now() < deadline, "removed service was not stopped/reaped");
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use crate::config::InactiveDimSettings;
|
||||
use ferese_layout::WindowId;
|
||||
use std::time::Duration;
|
||||
|
||||
use ferese_layout::WindowId;
|
||||
|
||||
use crate::config::InactiveDimSettings;
|
||||
|
||||
pub(crate) fn target(
|
||||
settings: InactiveDimSettings,
|
||||
focused: Option<WindowId>,
|
||||
@@ -60,11 +62,7 @@ impl DimAnimation {
|
||||
pub fn advance_visual(&mut self, target: f64, delta: Duration, duration_ms: f64) -> bool {
|
||||
self.advance(
|
||||
target,
|
||||
if self.target != target {
|
||||
Duration::ZERO
|
||||
} else {
|
||||
delta
|
||||
},
|
||||
if self.target != target { Duration::ZERO } else { delta },
|
||||
duration_ms,
|
||||
)
|
||||
}
|
||||
|
||||
+35
-78
@@ -1,27 +1,23 @@
|
||||
use std::sync::{
|
||||
Mutex,
|
||||
atomic::{AtomicBool, AtomicU64, Ordering},
|
||||
};
|
||||
use std::sync::Mutex;
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
|
||||
use ferese_protocols::effects::v1::server::{
|
||||
ferese_effects_manager_v1::{self, FereseEffectsManagerV1},
|
||||
ferese_surface_effects_v1::{self, FereseSurfaceEffectsV1},
|
||||
};
|
||||
use ferese_protocols::material::v1::server::{
|
||||
ferese_material_manager_v1::{self, FereseMaterialManagerV1},
|
||||
ferese_surface_material_v1::{self, FereseSurfaceMaterialV1},
|
||||
};
|
||||
use smithay::{
|
||||
reexports::wayland_server::{
|
||||
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, WEnum, Weak,
|
||||
backend::ClientId, protocol::wl_surface::WlSurface,
|
||||
},
|
||||
wayland::compositor::with_states,
|
||||
use ferese_protocols::effects::v1::server::ferese_effects_manager_v1::FereseEffectsManagerV1;
|
||||
use ferese_protocols::effects::v1::server::ferese_surface_effects_v1::FereseSurfaceEffectsV1;
|
||||
use ferese_protocols::effects::v1::server::{ferese_effects_manager_v1, ferese_surface_effects_v1};
|
||||
use ferese_protocols::material::v1::server::ferese_material_manager_v1::FereseMaterialManagerV1;
|
||||
use ferese_protocols::material::v1::server::ferese_surface_material_v1::FereseSurfaceMaterialV1;
|
||||
use ferese_protocols::material::v1::server::{ferese_material_manager_v1, ferese_surface_material_v1};
|
||||
use smithay::reexports::wayland_server::backend::ClientId;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::reexports::wayland_server::{
|
||||
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, WEnum, Weak,
|
||||
};
|
||||
use smithay::wayland::compositor::with_states;
|
||||
|
||||
use crate::{
|
||||
Ferese, config::MaterialStyle, private_client::ClientCapabilities, state::ClientState,
|
||||
};
|
||||
use crate::Ferese;
|
||||
use crate::config::MaterialStyle;
|
||||
use crate::private_client::ClientCapabilities;
|
||||
use crate::state::ClientState;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(crate) enum SemanticRole {
|
||||
@@ -41,16 +37,10 @@ pub(crate) struct ResolvedMaterial {
|
||||
pub shadow: [f64; 3],
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_material(
|
||||
role: SemanticRole,
|
||||
style: MaterialStyle,
|
||||
opacity: f32,
|
||||
) -> ResolvedMaterial {
|
||||
pub(crate) fn resolve_material(role: SemanticRole, style: MaterialStyle, opacity: f32) -> ResolvedMaterial {
|
||||
let shadow = match role {
|
||||
SemanticRole::Panel => [1.0, 5.0, 0.04],
|
||||
SemanticRole::PanelElevated | SemanticRole::Popover | SemanticRole::Menu => {
|
||||
[2.0, 8.0, 0.07]
|
||||
}
|
||||
SemanticRole::PanelElevated | SemanticRole::Popover | SemanticRole::Menu => [2.0, 8.0, 0.07],
|
||||
SemanticRole::Hud => [1.0, 4.0, 0.04],
|
||||
SemanticRole::Notification | SemanticRole::Modal => [3.0, 10.0, 0.09],
|
||||
};
|
||||
@@ -58,11 +48,7 @@ pub(crate) fn resolve_material(
|
||||
MaterialStyle::Solid => 1.0,
|
||||
MaterialStyle::Translucent => opacity.clamp(0.0, 1.0),
|
||||
};
|
||||
ResolvedMaterial {
|
||||
style,
|
||||
opacity,
|
||||
shadow,
|
||||
}
|
||||
ResolvedMaterial { style, opacity, shadow }
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -145,9 +131,7 @@ impl Dispatch<FereseMaterialManagerV1, ()> for Ferese {
|
||||
match request {
|
||||
ferese_material_manager_v1::Request::GetSurfaceMaterial { id, surface } => {
|
||||
let attached = with_states(&surface, |states| {
|
||||
states
|
||||
.data_map
|
||||
.insert_if_missing_threadsafe(SurfaceEffectsState::new);
|
||||
states.data_map.insert_if_missing_threadsafe(SurfaceEffectsState::new);
|
||||
let effects = states.data_map.get::<SurfaceEffectsState>().unwrap();
|
||||
effects.attached.swap(true, Ordering::AcqRel)
|
||||
});
|
||||
@@ -220,13 +204,9 @@ fn decode_regions(bytes: &[u8]) -> Option<Vec<[i32; 5]>> {
|
||||
.0
|
||||
.iter()
|
||||
.map(|tuple| {
|
||||
let values: [i32; 5] =
|
||||
std::array::from_fn(|index| i32::from_ne_bytes(tuple.as_chunks::<4>().0[index]));
|
||||
(values[2] > 0
|
||||
&& values[3] > 0
|
||||
&& values[4] >= 0
|
||||
&& values.iter().all(|v| v.abs_diff(0) <= 32768))
|
||||
.then_some(values)
|
||||
let values: [i32; 5] = std::array::from_fn(|index| i32::from_ne_bytes(tuple.as_chunks::<4>().0[index]));
|
||||
(values[2] > 0 && values[3] > 0 && values[4] >= 0 && values.iter().all(|v| v.abs_diff(0) <= 32768))
|
||||
.then_some(values)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -273,9 +253,7 @@ impl Dispatch<FereseEffectsManagerV1, ()> for Ferese {
|
||||
match request {
|
||||
ferese_effects_manager_v1::Request::GetSurfaceEffects { id, surface } => {
|
||||
let already_attached = with_states(&surface, |states| {
|
||||
states
|
||||
.data_map
|
||||
.insert_if_missing_threadsafe(SurfaceEffectsState::new);
|
||||
states.data_map.insert_if_missing_threadsafe(SurfaceEffectsState::new);
|
||||
states
|
||||
.data_map
|
||||
.get::<SurfaceEffectsState>()
|
||||
@@ -346,14 +324,10 @@ impl Dispatch<FereseSurfaceEffectsV1, SurfaceEffectsUserData> for Ferese {
|
||||
ferese_surface_effects_v1::Request::SetRole { role } => {
|
||||
let role = match role {
|
||||
WEnum::Value(ferese_surface_effects_v1::Role::Panel) => SemanticRole::Panel,
|
||||
WEnum::Value(ferese_surface_effects_v1::Role::PanelElevated) => {
|
||||
SemanticRole::PanelElevated
|
||||
}
|
||||
WEnum::Value(ferese_surface_effects_v1::Role::PanelElevated) => SemanticRole::PanelElevated,
|
||||
WEnum::Value(ferese_surface_effects_v1::Role::Popover) => SemanticRole::Popover,
|
||||
WEnum::Value(ferese_surface_effects_v1::Role::Menu) => SemanticRole::Menu,
|
||||
WEnum::Value(ferese_surface_effects_v1::Role::Notification) => {
|
||||
SemanticRole::Notification
|
||||
}
|
||||
WEnum::Value(ferese_surface_effects_v1::Role::Notification) => SemanticRole::Notification,
|
||||
WEnum::Value(ferese_surface_effects_v1::Role::Hud) => SemanticRole::Hud,
|
||||
WEnum::Value(ferese_surface_effects_v1::Role::Modal) => SemanticRole::Modal,
|
||||
WEnum::Unknown(_) | WEnum::Value(_) => return,
|
||||
@@ -437,22 +411,17 @@ pub(crate) fn surface_opacity(surface: &WlSurface) -> f32 {
|
||||
states
|
||||
.data_map
|
||||
.get::<SurfaceEffectsState>()
|
||||
.map_or(1.0, |effects| {
|
||||
*effects.opacity.lock().unwrap() as f32 / 1000.0
|
||||
})
|
||||
.map_or(1.0, |effects| *effects.opacity.lock().unwrap() as f32 / 1000.0)
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn begin_surface_dismiss(surface: &WlSurface) -> bool {
|
||||
with_states(surface, |states| {
|
||||
states
|
||||
.data_map
|
||||
.get::<SurfaceEffectsState>()
|
||||
.is_some_and(|effects| {
|
||||
effects.presentation_supported.load(Ordering::Acquire)
|
||||
&& effects.attached.load(Ordering::Acquire)
|
||||
&& !effects.dismissing.swap(true, Ordering::AcqRel)
|
||||
})
|
||||
states.data_map.get::<SurfaceEffectsState>().is_some_and(|effects| {
|
||||
effects.presentation_supported.load(Ordering::Acquire)
|
||||
&& effects.attached.load(Ordering::Acquire)
|
||||
&& !effects.dismissing.swap(true, Ordering::AcqRel)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -477,20 +446,8 @@ mod tests {
|
||||
assert_eq!(decode_regions(&[]), Some(vec![]));
|
||||
assert!(decode_regions(&bytes[..19]).is_none());
|
||||
assert!(decode_regions(&bytes.repeat(33)).is_none());
|
||||
for invalid in [
|
||||
[0_i32, 0, 0, 48, 11],
|
||||
[0, 0, 120, 48, -1],
|
||||
[i32::MIN, 0, 120, 48, 11],
|
||||
] {
|
||||
assert!(
|
||||
decode_regions(
|
||||
&invalid
|
||||
.into_iter()
|
||||
.flat_map(i32::to_ne_bytes)
|
||||
.collect::<Vec<_>>()
|
||||
)
|
||||
.is_none()
|
||||
);
|
||||
for invalid in [[0_i32, 0, 0, 48, 11], [0, 0, 120, 48, -1], [i32::MIN, 0, 120, 48, 11]] {
|
||||
assert!(decode_regions(&invalid.into_iter().flat_map(i32::to_ne_bytes).collect::<Vec<_>>()).is_none());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
use smithay::{
|
||||
desktop::Window,
|
||||
input::pointer::{
|
||||
AxisFrame, ButtonEvent, GestureHoldBeginEvent, GestureHoldEndEvent, GesturePinchBeginEvent,
|
||||
GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent,
|
||||
GestureSwipeEndEvent, GestureSwipeUpdateEvent, GrabStartData, MotionEvent, PointerGrab,
|
||||
PointerInnerHandle, RelativeMotionEvent,
|
||||
},
|
||||
reexports::wayland_server::protocol::wl_surface::WlSurface,
|
||||
utils::{Logical, Point, Size},
|
||||
use smithay::desktop::Window;
|
||||
use smithay::input::pointer::{
|
||||
AxisFrame, ButtonEvent, GestureHoldBeginEvent, GestureHoldEndEvent, GesturePinchBeginEvent, GesturePinchEndEvent,
|
||||
GesturePinchUpdateEvent, GestureSwipeBeginEvent, GestureSwipeEndEvent, GestureSwipeUpdateEvent, GrabStartData,
|
||||
MotionEvent, PointerGrab, PointerInnerHandle, RelativeMotionEvent,
|
||||
};
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::utils::{Logical, Point, Size};
|
||||
|
||||
use crate::Ferese;
|
||||
|
||||
@@ -44,12 +41,7 @@ impl PointerGrab<Ferese> for MoveSurfaceGrab {
|
||||
handle.relative_motion(data, focus, event);
|
||||
}
|
||||
|
||||
fn button(
|
||||
&mut self,
|
||||
data: &mut Ferese,
|
||||
handle: &mut PointerInnerHandle<'_, Ferese>,
|
||||
event: &ButtonEvent,
|
||||
) {
|
||||
fn button(&mut self, data: &mut Ferese, handle: &mut PointerInnerHandle<'_, Ferese>, event: &ButtonEvent) {
|
||||
handle.button(data, event);
|
||||
if handle.current_pressed().is_empty() {
|
||||
self.finished = true;
|
||||
@@ -57,12 +49,7 @@ impl PointerGrab<Ferese> for MoveSurfaceGrab {
|
||||
}
|
||||
}
|
||||
|
||||
fn axis(
|
||||
&mut self,
|
||||
data: &mut Ferese,
|
||||
handle: &mut PointerInnerHandle<'_, Ferese>,
|
||||
frame: AxisFrame,
|
||||
) {
|
||||
fn axis(&mut self, data: &mut Ferese, handle: &mut PointerInnerHandle<'_, Ferese>, frame: AxisFrame) {
|
||||
handle.axis(data, frame);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
use std::cell::RefCell;
|
||||
|
||||
use smithay::{
|
||||
desktop::Window,
|
||||
input::pointer::{
|
||||
AxisFrame, ButtonEvent, GestureHoldBeginEvent, GestureHoldEndEvent, GesturePinchBeginEvent,
|
||||
GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent,
|
||||
GestureSwipeEndEvent, GestureSwipeUpdateEvent, GrabStartData, MotionEvent, PointerGrab,
|
||||
PointerInnerHandle, RelativeMotionEvent,
|
||||
},
|
||||
reexports::{
|
||||
wayland_protocols::xdg::shell::server::xdg_toplevel,
|
||||
wayland_server::protocol::wl_surface::WlSurface,
|
||||
},
|
||||
utils::{Logical, Point, Rectangle, Size},
|
||||
wayland::{compositor::with_states, shell::xdg::SurfaceCachedState},
|
||||
use smithay::desktop::Window;
|
||||
use smithay::input::pointer::{
|
||||
AxisFrame, ButtonEvent, GestureHoldBeginEvent, GestureHoldEndEvent, GesturePinchBeginEvent, GesturePinchEndEvent,
|
||||
GesturePinchUpdateEvent, GestureSwipeBeginEvent, GestureSwipeEndEvent, GestureSwipeUpdateEvent, GrabStartData,
|
||||
MotionEvent, PointerGrab, PointerInnerHandle, RelativeMotionEvent,
|
||||
};
|
||||
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::utils::{Logical, Point, Rectangle, Size};
|
||||
use smithay::wayland::compositor::with_states;
|
||||
use smithay::wayland::shell::xdg::SurfaceCachedState;
|
||||
|
||||
use crate::Ferese;
|
||||
|
||||
@@ -31,9 +27,7 @@ impl ResizeEdge {
|
||||
fn left(self) -> bool {
|
||||
matches!(
|
||||
self.0,
|
||||
xdg_toplevel::ResizeEdge::Left
|
||||
| xdg_toplevel::ResizeEdge::TopLeft
|
||||
| xdg_toplevel::ResizeEdge::BottomLeft
|
||||
xdg_toplevel::ResizeEdge::Left | xdg_toplevel::ResizeEdge::TopLeft | xdg_toplevel::ResizeEdge::BottomLeft
|
||||
)
|
||||
}
|
||||
|
||||
@@ -49,9 +43,7 @@ impl ResizeEdge {
|
||||
fn top(self) -> bool {
|
||||
matches!(
|
||||
self.0,
|
||||
xdg_toplevel::ResizeEdge::Top
|
||||
| xdg_toplevel::ResizeEdge::TopLeft
|
||||
| xdg_toplevel::ResizeEdge::TopRight
|
||||
xdg_toplevel::ResizeEdge::Top | xdg_toplevel::ResizeEdge::TopLeft | xdg_toplevel::ResizeEdge::TopRight
|
||||
)
|
||||
}
|
||||
|
||||
@@ -82,16 +74,8 @@ impl ResizeSurfaceGrab {
|
||||
initial_rect: Rectangle<i32, Logical>,
|
||||
) -> Self {
|
||||
ResizeState::with(
|
||||
window
|
||||
.toplevel()
|
||||
.expect("managed window has a toplevel")
|
||||
.wl_surface(),
|
||||
|state| {
|
||||
*state = ResizeState::Resizing {
|
||||
edges,
|
||||
initial_rect,
|
||||
}
|
||||
},
|
||||
window.toplevel().expect("managed window has a toplevel").wl_surface(),
|
||||
|state| *state = ResizeState::Resizing { edges, initial_rect },
|
||||
);
|
||||
Self {
|
||||
start_data,
|
||||
@@ -114,17 +98,13 @@ impl PointerGrab<Ferese> for ResizeSurfaceGrab {
|
||||
) {
|
||||
handle.motion(data, None, event);
|
||||
let delta = event.location - self.start_data.location;
|
||||
let surface = self
|
||||
.window
|
||||
.toplevel()
|
||||
.expect("managed window has a toplevel");
|
||||
let surface = self.window.toplevel().expect("managed window has a toplevel");
|
||||
let (minimum, maximum) = with_states(surface.wl_surface(), |states| {
|
||||
let mut cached = states.cached_state.get::<SurfaceCachedState>();
|
||||
let state = cached.current();
|
||||
(state.min_size, state.max_size)
|
||||
});
|
||||
self.last_size =
|
||||
constrained_size(self.initial_rect.size, delta, self.edges, minimum, maximum);
|
||||
self.last_size = constrained_size(self.initial_rect.size, delta, self.edges, minimum, maximum);
|
||||
let rect = resized_rect(self.initial_rect, self.last_size, self.edges);
|
||||
data.set_floating_window_geometry(&self.window, rect.loc, rect.size);
|
||||
|
||||
@@ -145,20 +125,12 @@ impl PointerGrab<Ferese> for ResizeSurfaceGrab {
|
||||
handle.relative_motion(data, focus, event);
|
||||
}
|
||||
|
||||
fn button(
|
||||
&mut self,
|
||||
data: &mut Ferese,
|
||||
handle: &mut PointerInnerHandle<'_, Ferese>,
|
||||
event: &ButtonEvent,
|
||||
) {
|
||||
fn button(&mut self, data: &mut Ferese, handle: &mut PointerInnerHandle<'_, Ferese>, event: &ButtonEvent) {
|
||||
handle.button(data, event);
|
||||
if handle.current_pressed().is_empty() {
|
||||
self.finished = true;
|
||||
handle.unset_grab(self, data, event.serial, event.time, true);
|
||||
let surface = self
|
||||
.window
|
||||
.toplevel()
|
||||
.expect("managed window has a toplevel");
|
||||
let surface = self.window.toplevel().expect("managed window has a toplevel");
|
||||
surface.with_pending_state(|state| {
|
||||
state.states.unset(xdg_toplevel::State::Resizing);
|
||||
state.size = Some(self.last_size);
|
||||
@@ -173,12 +145,7 @@ impl PointerGrab<Ferese> for ResizeSurfaceGrab {
|
||||
}
|
||||
}
|
||||
|
||||
fn axis(
|
||||
&mut self,
|
||||
data: &mut Ferese,
|
||||
handle: &mut PointerInnerHandle<'_, Ferese>,
|
||||
frame: AxisFrame,
|
||||
) {
|
||||
fn axis(&mut self, data: &mut Ferese, handle: &mut PointerInnerHandle<'_, Ferese>, frame: AxisFrame) {
|
||||
handle.axis(data, frame);
|
||||
}
|
||||
|
||||
@@ -267,21 +234,14 @@ impl PointerGrab<Ferese> for ResizeSurfaceGrab {
|
||||
return;
|
||||
}
|
||||
|
||||
let surface = self
|
||||
.window
|
||||
.toplevel()
|
||||
.expect("managed window has a toplevel");
|
||||
let surface = self.window.toplevel().expect("managed window has a toplevel");
|
||||
surface.with_pending_state(|state| {
|
||||
state.states.unset(xdg_toplevel::State::Resizing);
|
||||
state.size = Some(self.initial_rect.size);
|
||||
});
|
||||
surface.send_pending_configure();
|
||||
ResizeState::with(surface.wl_surface(), |state| *state = ResizeState::Idle);
|
||||
data.set_floating_window_geometry(
|
||||
&self.window,
|
||||
self.initial_rect.loc,
|
||||
self.initial_rect.size,
|
||||
);
|
||||
data.set_floating_window_geometry(&self.window, self.initial_rect.loc, self.initial_rect.size);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,14 +276,8 @@ impl ResizeState {
|
||||
fn commit(&mut self) -> Option<(ResizeEdge, Rectangle<i32, Logical>)> {
|
||||
match *self {
|
||||
Self::Idle => None,
|
||||
Self::Resizing {
|
||||
edges,
|
||||
initial_rect,
|
||||
} => Some((edges, initial_rect)),
|
||||
Self::WaitingForFinalCommit {
|
||||
edges,
|
||||
initial_rect,
|
||||
} => {
|
||||
Self::Resizing { edges, initial_rect } => Some((edges, initial_rect)),
|
||||
Self::WaitingForFinalCommit { edges, initial_rect } => {
|
||||
*self = Self::Idle;
|
||||
Some((edges, initial_rect))
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
+193
-112
@@ -1,53 +1,122 @@
|
||||
use smithay::reexports::wayland_server::Resource;
|
||||
use std::process::Command;
|
||||
|
||||
use ferese_layout::Direction;
|
||||
use smithay::{
|
||||
backend::input::{
|
||||
AbsolutePositionEvent, Axis, AxisSource, ButtonState, Event, GestureBeginEvent,
|
||||
GestureEndEvent, GestureSwipeUpdateEvent as _, InputBackend, InputEvent, KeyState,
|
||||
KeyboardKeyEvent, PointerAxisEvent, PointerButtonEvent, PointerMotionEvent, Switch,
|
||||
SwitchState, SwitchToggleEvent, TouchEvent,
|
||||
},
|
||||
input::{
|
||||
keyboard::{FilterResult, keysyms},
|
||||
pointer::{
|
||||
AxisFrame, ButtonEvent, Focus, GestureSwipeBeginEvent, GestureSwipeEndEvent,
|
||||
GestureSwipeUpdateEvent, GrabStartData, MotionEvent, PointerHandle,
|
||||
RelativeMotionEvent,
|
||||
},
|
||||
touch::{DownEvent, MotionEvent as TouchMotionEvent, UpEvent},
|
||||
},
|
||||
reexports::{
|
||||
wayland_protocols::xdg::shell::server::xdg_toplevel::ResizeEdge as XdgResizeEdge,
|
||||
wayland_server::protocol::wl_surface::WlSurface,
|
||||
},
|
||||
utils::{Logical, Point, SERIAL_COUNTER, Serial},
|
||||
wayland::{
|
||||
keyboard_shortcuts_inhibit::KeyboardShortcutsInhibitorSeat,
|
||||
pointer_constraints::{PointerConstraint, with_pointer_constraint},
|
||||
},
|
||||
use smithay::backend::input::{
|
||||
AbsolutePositionEvent, Axis, AxisSource, ButtonState, Event, GestureBeginEvent, GestureEndEvent,
|
||||
GestureSwipeUpdateEvent as _, InputBackend, InputEvent, KeyState, KeyboardKeyEvent, PointerAxisEvent,
|
||||
PointerButtonEvent, PointerMotionEvent, Switch, SwitchState, SwitchToggleEvent, TouchEvent,
|
||||
};
|
||||
use smithay::input::keyboard::{FilterResult, keysyms};
|
||||
use smithay::input::pointer::{
|
||||
AxisFrame, ButtonEvent, Focus, GestureSwipeBeginEvent, GestureSwipeEndEvent, GestureSwipeUpdateEvent,
|
||||
GrabStartData, MotionEvent, PointerHandle, RelativeMotionEvent,
|
||||
};
|
||||
use smithay::input::touch::{DownEvent, MotionEvent as TouchMotionEvent, UpEvent};
|
||||
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::ResizeEdge as XdgResizeEdge;
|
||||
use smithay::reexports::wayland_server::Resource;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::utils::{Logical, Point, SERIAL_COUNTER, Serial};
|
||||
use smithay::wayland::keyboard_shortcuts_inhibit::KeyboardShortcutsInhibitorSeat;
|
||||
use smithay::wayland::pointer_constraints::{PointerConstraint, with_pointer_constraint};
|
||||
|
||||
use crate::{
|
||||
Ferese,
|
||||
config::BindingAction,
|
||||
gestures::SwipeDirection,
|
||||
grabs::{MoveSurfaceGrab, ResizeEdge, ResizeSurfaceGrab},
|
||||
};
|
||||
use crate::Ferese;
|
||||
use crate::config::BindingAction;
|
||||
use crate::gestures::SwipeDirection;
|
||||
use crate::grabs::{MoveSurfaceGrab, ResizeEdge, ResizeSurfaceGrab};
|
||||
|
||||
const BTN_LEFT: u32 = 0x110;
|
||||
const BTN_RIGHT: u32 = 0x111;
|
||||
|
||||
pub(crate) struct LockedPointerHint {
|
||||
pub(crate) surface: WlSurface,
|
||||
pub(crate) local: Point<f64, Logical>,
|
||||
}
|
||||
|
||||
impl Ferese {
|
||||
pub(crate) fn pointer_hint_position(
|
||||
&self,
|
||||
surface: &WlSurface,
|
||||
hint: Point<f64, Logical>,
|
||||
) -> Option<Point<f64, Logical>> {
|
||||
for window in self.space.elements() {
|
||||
let root = window.toplevel()?.wl_surface();
|
||||
let geometry = window.geometry();
|
||||
let Some(local) = surface_tree_position(root, surface, geometry.loc) else {
|
||||
continue;
|
||||
};
|
||||
let id = self.window_ids.get(window)?;
|
||||
let presented = self.presented_window_rect(*id)?;
|
||||
let (scale_x, scale_y) = self.visual_scale_for_window(window)?;
|
||||
let point = local.to_f64() + hint - geometry.loc.to_f64();
|
||||
return Some(Point::from((
|
||||
presented.x + point.x * scale_x,
|
||||
presented.y + point.y * scale_y,
|
||||
)));
|
||||
}
|
||||
for output in self.space.outputs() {
|
||||
let output_geometry = self.space.output_geometry(output)?;
|
||||
let layers = smithay::desktop::layer_map_for_output(output);
|
||||
for layer in layers.layers() {
|
||||
let Some(local) = surface_tree_position(layer.wl_surface(), surface, (0, 0).into()) else {
|
||||
continue;
|
||||
};
|
||||
let geometry = layers.layer_geometry(layer)?;
|
||||
return Some((output_geometry.loc + geometry.loc + local).to_f64() + hint);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) fn apply_unlocked_pointer_hint(&mut self) {
|
||||
let Some(hint) = self.locked_pointer_hint.take() else {
|
||||
return;
|
||||
};
|
||||
if !hint.surface.is_alive() || self.session_lock.active || self.input_capture.active() {
|
||||
return;
|
||||
}
|
||||
let Some(pointer) = self.seat.get_pointer() else {
|
||||
return;
|
||||
};
|
||||
let locked = with_pointer_constraint(&hint.surface, &pointer, |constraint| {
|
||||
constraint.is_some_and(|constraint| {
|
||||
constraint.is_active() && matches!(&*constraint, PointerConstraint::Locked(_))
|
||||
})
|
||||
});
|
||||
if locked {
|
||||
self.locked_pointer_hint = Some(hint);
|
||||
return;
|
||||
}
|
||||
if pointer.current_focus().is_some_and(|surface| {
|
||||
with_pointer_constraint(&surface, &pointer, |constraint| {
|
||||
constraint.is_some_and(|constraint| constraint.is_active())
|
||||
})
|
||||
}) {
|
||||
return;
|
||||
}
|
||||
let Some(global) = self.pointer_hint_position(&hint.surface, hint.local) else {
|
||||
return;
|
||||
};
|
||||
let location = self.clamp_pointer_position(global);
|
||||
let focus = self.surface_under(location);
|
||||
pointer.motion(
|
||||
self,
|
||||
focus,
|
||||
&MotionEvent {
|
||||
location,
|
||||
serial: SERIAL_COUNTER.next_serial(),
|
||||
time: self.last_pointer_time,
|
||||
},
|
||||
);
|
||||
pointer.frame(self);
|
||||
self.cursor_redraw_pending = true;
|
||||
}
|
||||
|
||||
pub fn process_input_event<I: InputBackend>(&mut self, event: InputEvent<I>) {
|
||||
let seat = self.seat.clone();
|
||||
self.idle_notifier_state.notify_activity(&seat);
|
||||
if !matches!(
|
||||
&event,
|
||||
InputEvent::DeviceAdded { .. }
|
||||
| InputEvent::DeviceRemoved { .. }
|
||||
| InputEvent::SwitchToggle { .. }
|
||||
InputEvent::DeviceAdded { .. } | InputEvent::DeviceRemoved { .. } | InputEvent::SwitchToggle { .. }
|
||||
) {
|
||||
self.lock_input_activity();
|
||||
}
|
||||
@@ -117,15 +186,13 @@ impl Ferese {
|
||||
if self.swipe.active() {
|
||||
self.swipe.update(event.delta_x(), event.delta_y());
|
||||
} else {
|
||||
seat.get_pointer()
|
||||
.expect("seat has a pointer")
|
||||
.gesture_swipe_update(
|
||||
self,
|
||||
&GestureSwipeUpdateEvent {
|
||||
time: event.time_msec(),
|
||||
delta: event.delta(),
|
||||
},
|
||||
);
|
||||
seat.get_pointer().expect("seat has a pointer").gesture_swipe_update(
|
||||
self,
|
||||
&GestureSwipeUpdateEvent {
|
||||
time: event.time_msec(),
|
||||
delta: event.delta(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
InputEvent::GestureSwipeEnd { event } => {
|
||||
@@ -140,9 +207,7 @@ impl Ferese {
|
||||
.map(|binding| binding.action.clone())
|
||||
{
|
||||
match action {
|
||||
BindingAction::Focus(
|
||||
direction @ (Direction::Left | Direction::Right),
|
||||
) => {
|
||||
BindingAction::Focus(direction @ (Direction::Left | Direction::Right)) => {
|
||||
self.focus_direction_from_swipe(direction);
|
||||
}
|
||||
BindingAction::SwitchRelativeWorkspace(next) => {
|
||||
@@ -152,16 +217,14 @@ impl Ferese {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
seat.get_pointer()
|
||||
.expect("seat has a pointer")
|
||||
.gesture_swipe_end(
|
||||
self,
|
||||
&GestureSwipeEndEvent {
|
||||
serial: SERIAL_COUNTER.next_serial(),
|
||||
time: event.time_msec(),
|
||||
cancelled: event.cancelled(),
|
||||
},
|
||||
);
|
||||
seat.get_pointer().expect("seat has a pointer").gesture_swipe_end(
|
||||
self,
|
||||
&GestureSwipeEndEvent {
|
||||
serial: SERIAL_COUNTER.next_serial(),
|
||||
time: event.time_msec(),
|
||||
cancelled: event.cancelled(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
InputEvent::SwitchToggle { event } if event.switch() == Some(Switch::Lid) => {
|
||||
@@ -301,6 +364,7 @@ impl Ferese {
|
||||
}
|
||||
}
|
||||
InputEvent::PointerMotionAbsolute { event, .. } => {
|
||||
self.last_pointer_time = event.time_msec();
|
||||
let Some(position) = self.absolute_event_position(&event) else {
|
||||
return;
|
||||
};
|
||||
@@ -325,6 +389,7 @@ impl Ferese {
|
||||
crate::backends::direct::render_all(self);
|
||||
}
|
||||
InputEvent::PointerMotion { event, .. } => {
|
||||
self.last_pointer_time = event.time_msec();
|
||||
let pointer = self.seat.get_pointer().expect("seat has a pointer");
|
||||
let position = pointer.current_location();
|
||||
let focus = self.surface_under(position);
|
||||
@@ -346,8 +411,7 @@ impl Ferese {
|
||||
focus,
|
||||
&RelativeMotionEvent {
|
||||
delta: (delta.x / scale_x, delta.y / scale_y).into(),
|
||||
delta_unaccel: (delta_unaccel.x / scale_x, delta_unaccel.y / scale_y)
|
||||
.into(),
|
||||
delta_unaccel: (delta_unaccel.x / scale_x, delta_unaccel.y / scale_y).into(),
|
||||
utime: event.time(),
|
||||
},
|
||||
);
|
||||
@@ -369,6 +433,7 @@ impl Ferese {
|
||||
crate::backends::direct::render_all(self);
|
||||
}
|
||||
InputEvent::PointerButton { event, .. } => {
|
||||
self.last_pointer_time = event.time_msec();
|
||||
if self.input_capture.active() {
|
||||
if self.input_capture.captures(2) {
|
||||
self.input_capture.push(serde_json::json!({"type":"button", "button":event.button_code(), "pressed":event.state() == ButtonState::Pressed}));
|
||||
@@ -392,11 +457,8 @@ impl Ferese {
|
||||
&& crate::effects::begin_surface_dismiss(&surface)
|
||||
{
|
||||
let opacity = crate::effects::surface_opacity(&surface);
|
||||
self.dismissing_popups.push((
|
||||
root,
|
||||
popup,
|
||||
crate::dimming::DimAnimation::new(f64::from(opacity)),
|
||||
));
|
||||
self.dismissing_popups
|
||||
.push((root, popup, crate::dimming::DimAnimation::new(f64::from(opacity))));
|
||||
pointer.unset_grab(self, serial, event.time_msec());
|
||||
self.focus_window_at(pointer.current_location(), serial, true);
|
||||
crate::backends::direct::render_all(self);
|
||||
@@ -466,13 +528,14 @@ impl Ferese {
|
||||
pointer.frame(self);
|
||||
}
|
||||
InputEvent::PointerAxis { event, .. } => {
|
||||
self.last_pointer_time = event.time_msec();
|
||||
let source = event.source();
|
||||
let horizontal = event.amount(Axis::Horizontal).unwrap_or_else(|| {
|
||||
event.amount_v120(Axis::Horizontal).unwrap_or(0.0) * 15.0 / 120.0
|
||||
});
|
||||
let vertical = event.amount(Axis::Vertical).unwrap_or_else(|| {
|
||||
event.amount_v120(Axis::Vertical).unwrap_or(0.0) * 15.0 / 120.0
|
||||
});
|
||||
let horizontal = event
|
||||
.amount(Axis::Horizontal)
|
||||
.unwrap_or_else(|| event.amount_v120(Axis::Horizontal).unwrap_or(0.0) * 15.0 / 120.0);
|
||||
let vertical = event
|
||||
.amount(Axis::Vertical)
|
||||
.unwrap_or_else(|| event.amount_v120(Axis::Vertical).unwrap_or(0.0) * 15.0 / 120.0);
|
||||
if self.input_capture.active() {
|
||||
if self.input_capture.captures(2) {
|
||||
self.input_capture.push(serde_json::json!({"type":"scroll", "x":horizontal, "y":vertical, "v120_x":event.amount_v120(Axis::Horizontal), "v120_y":event.amount_v120(Axis::Vertical), "stop_x":source == AxisSource::Finger && event.amount(Axis::Horizontal) == Some(0.0), "stop_y":source == AxisSource::Finger && event.amount(Axis::Vertical) == Some(0.0)}));
|
||||
@@ -482,11 +545,7 @@ impl Ferese {
|
||||
let pointer = self.seat.get_pointer().expect("seat has a pointer");
|
||||
if self.scroll_overview_strip(
|
||||
pointer.current_location(),
|
||||
if horizontal != 0.0 {
|
||||
horizontal
|
||||
} else {
|
||||
vertical
|
||||
},
|
||||
if horizontal != 0.0 { horizontal } else { vertical },
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -577,12 +636,7 @@ impl Ferese {
|
||||
}
|
||||
}
|
||||
|
||||
fn start_floating_pointer_grab(
|
||||
&mut self,
|
||||
pointer: &PointerHandle<Self>,
|
||||
button: u32,
|
||||
serial: Serial,
|
||||
) -> bool {
|
||||
fn start_floating_pointer_grab(&mut self, pointer: &PointerHandle<Self>, button: u32, serial: Serial) -> bool {
|
||||
if self.session_lock.active
|
||||
|| pointer.is_grabbed()
|
||||
|| !matches!(button, BTN_LEFT | BTN_RIGHT)
|
||||
@@ -629,10 +683,8 @@ impl Ferese {
|
||||
Focus::Clear,
|
||||
);
|
||||
} else {
|
||||
let horizontal_right =
|
||||
location.x >= f64::from(rect.loc.x) + f64::from(rect.size.w) / 2.0;
|
||||
let vertical_bottom =
|
||||
location.y >= f64::from(rect.loc.y) + f64::from(rect.size.h) / 2.0;
|
||||
let horizontal_right = location.x >= f64::from(rect.loc.x) + f64::from(rect.size.w) / 2.0;
|
||||
let vertical_bottom = location.y >= f64::from(rect.loc.y) + f64::from(rect.size.h) / 2.0;
|
||||
let edge = match (horizontal_right, vertical_bottom) {
|
||||
(false, false) => XdgResizeEdge::TopLeft,
|
||||
(true, false) => XdgResizeEdge::TopRight,
|
||||
@@ -655,9 +707,7 @@ impl Ferese {
|
||||
I: InputBackend,
|
||||
E: AbsolutePositionEvent<I>,
|
||||
{
|
||||
let output = self
|
||||
.focused_output()
|
||||
.or_else(|| self.space.outputs().next())?;
|
||||
let output = self.focused_output().or_else(|| self.space.outputs().next())?;
|
||||
let geometry = self.space.output_geometry(output)?;
|
||||
|
||||
Some(event.position_transformed(geometry.size) + geometry.loc.to_f64())
|
||||
@@ -666,14 +716,8 @@ impl Ferese {
|
||||
fn swipe_navigation_blocked(&self) -> bool {
|
||||
self.session_lock.active
|
||||
|| self.active_shortcuts_inhibitor.is_some()
|
||||
|| self
|
||||
.seat
|
||||
.get_pointer()
|
||||
.is_some_and(|pointer| pointer.is_grabbed())
|
||||
|| self
|
||||
.seat
|
||||
.get_keyboard()
|
||||
.is_some_and(|keyboard| keyboard.is_grabbed())
|
||||
|| self.seat.get_pointer().is_some_and(|pointer| pointer.is_grabbed())
|
||||
|| self.seat.get_keyboard().is_some_and(|keyboard| keyboard.is_grabbed())
|
||||
}
|
||||
|
||||
fn switch_relative_workspace(&mut self, next: bool, slide: Option<SwipeDirection>) {
|
||||
@@ -689,10 +733,7 @@ impl Ferese {
|
||||
.output_for_workspace(workspace.id)
|
||||
.is_none_or(|owner| owner == output)
|
||||
});
|
||||
let Some(index) = candidates
|
||||
.iter()
|
||||
.position(|workspace| workspace.id == current)
|
||||
else {
|
||||
let Some(index) = candidates.iter().position(|workspace| workspace.id == current) else {
|
||||
return;
|
||||
};
|
||||
let index = if next {
|
||||
@@ -799,11 +840,7 @@ impl Ferese {
|
||||
}
|
||||
}
|
||||
|
||||
fn focus_window_under_pointer(
|
||||
&mut self,
|
||||
pointer: &PointerHandle<Self>,
|
||||
position: Point<f64, Logical>,
|
||||
) {
|
||||
fn focus_window_under_pointer(&mut self, pointer: &PointerHandle<Self>, position: Point<f64, Logical>) {
|
||||
if self.session_lock.active {
|
||||
return;
|
||||
}
|
||||
@@ -813,10 +850,7 @@ impl Ferese {
|
||||
}
|
||||
// Keep the explicitly selected window focused until Overview's exit
|
||||
// animation settles. Moving previews must not steal focus on pointer jitter.
|
||||
if self.overview.is_presenting()
|
||||
|| !self.input_settings.focus_follows_mouse
|
||||
|| pointer.is_grabbed()
|
||||
{
|
||||
if self.overview.is_presenting() || !self.input_settings.focus_follows_mouse || pointer.is_grabbed() {
|
||||
return;
|
||||
}
|
||||
if self.layer_under(position).is_some() {
|
||||
@@ -952,9 +986,7 @@ impl Ferese {
|
||||
BindingAction::Focus(direction) => self.focus_direction(direction),
|
||||
BindingAction::Move(direction) => self.move_direction(direction),
|
||||
BindingAction::Resize(direction) => self.resize_direction(direction),
|
||||
BindingAction::SwitchRelativeWorkspace(next) => {
|
||||
self.switch_relative_workspace(next, None)
|
||||
}
|
||||
BindingAction::SwitchRelativeWorkspace(next) => self.switch_relative_workspace(next, None),
|
||||
BindingAction::SwitchWorkspace(workspace) => {
|
||||
self.switch_workspace(u32::from(workspace));
|
||||
}
|
||||
@@ -974,6 +1006,55 @@ impl Ferese {
|
||||
}
|
||||
}
|
||||
|
||||
fn surface_tree_position(
|
||||
root: &WlSurface,
|
||||
target: &WlSurface,
|
||||
geometry_origin: Point<i32, Logical>,
|
||||
) -> Option<Point<i32, Logical>> {
|
||||
use smithay::desktop::PopupManager;
|
||||
|
||||
surface_tree_offset(root, target).or_else(|| {
|
||||
PopupManager::popups_for_surface(root).find_map(|(popup, offset)| {
|
||||
surface_tree_offset(popup.wl_surface(), target)
|
||||
.map(|local| geometry_origin + offset - popup.geometry().loc + local)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn surface_tree_offset(root: &WlSurface, target: &WlSurface) -> Option<Point<i32, Logical>> {
|
||||
use std::cell::Cell;
|
||||
|
||||
use smithay::backend::renderer::utils::RendererSurfaceStateUserData;
|
||||
use smithay::wayland::compositor::{TraversalAction, with_surface_tree_downward};
|
||||
|
||||
let found = Cell::new(None);
|
||||
with_surface_tree_downward(
|
||||
root,
|
||||
Point::<i32, Logical>::default(),
|
||||
|_, states, location| {
|
||||
states
|
||||
.data_map
|
||||
.get::<RendererSurfaceStateUserData>()
|
||||
.and_then(|data| data.lock().unwrap().view())
|
||||
.map_or(TraversalAction::SkipChildren, |view| {
|
||||
TraversalAction::DoChildren(*location + view.offset)
|
||||
})
|
||||
},
|
||||
|surface, states, location| {
|
||||
if surface == target
|
||||
&& let Some(view) = states
|
||||
.data_map
|
||||
.get::<RendererSurfaceStateUserData>()
|
||||
.and_then(|data| data.lock().unwrap().view())
|
||||
{
|
||||
found.set(Some(*location + view.offset));
|
||||
}
|
||||
},
|
||||
|_, _, _| found.get().is_none(),
|
||||
);
|
||||
found.get()
|
||||
}
|
||||
|
||||
fn virtual_terminal(symbol: u32, ctrl: bool, alt: bool, direct: bool) -> Option<i32> {
|
||||
if !direct {
|
||||
return None;
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
use std::{
|
||||
collections::{HashMap, VecDeque},
|
||||
sync::mpsc::SyncSender,
|
||||
};
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::sync::mpsc::SyncSender;
|
||||
|
||||
use ferese_ipc::Response;
|
||||
use serde::Deserialize;
|
||||
@@ -91,24 +89,14 @@ fn edge(barrier: &Barrier, zones: &[Zone]) -> Option<Edge> {
|
||||
}
|
||||
match found {
|
||||
Edge::Top | Edge::Bottom => {
|
||||
let outside = y1
|
||||
+ if matches!(found, Edge::Top) {
|
||||
-0.5
|
||||
} else {
|
||||
0.5
|
||||
};
|
||||
let outside = y1 + if matches!(found, Edge::Top) { -0.5 } else { 0.5 };
|
||||
outside >= f64::from(other.y)
|
||||
&& outside < other.bottom()
|
||||
&& x1 < other.right()
|
||||
&& x2 + 1.0 > f64::from(other.x)
|
||||
}
|
||||
Edge::Left | Edge::Right => {
|
||||
let outside = x1
|
||||
+ if matches!(found, Edge::Left) {
|
||||
-0.5
|
||||
} else {
|
||||
0.5
|
||||
};
|
||||
let outside = x1 + if matches!(found, Edge::Left) { -0.5 } else { 0.5 };
|
||||
outside >= f64::from(other.x)
|
||||
&& outside < other.right()
|
||||
&& y1 < other.bottom()
|
||||
@@ -186,10 +174,7 @@ impl InputCapture {
|
||||
if self.sessions.len() >= MAX_SESSIONS || capabilities == 0 || capabilities & !3 != 0 {
|
||||
return Err("Unsupported capabilities or input capture session limit reached".into());
|
||||
}
|
||||
self.next_session = self
|
||||
.next_session
|
||||
.checked_add(1)
|
||||
.ok_or("Input capture IDs exhausted")?;
|
||||
self.next_session = self.next_session.checked_add(1).ok_or("Input capture IDs exhausted")?;
|
||||
let id = self.next_session;
|
||||
self.sessions.insert(
|
||||
id,
|
||||
@@ -208,24 +193,15 @@ impl InputCapture {
|
||||
|
||||
pub(crate) fn zones(&self, id: u64) -> Result<Value, String> {
|
||||
self.session(id)?;
|
||||
Ok(
|
||||
json!({"zone_set":self.zone_set, "zones":self.zones.iter().map(|zone|
|
||||
(zone.width, zone.height, zone.x, zone.y)).collect::<Vec<_>>()}),
|
||||
)
|
||||
Ok(json!({"zone_set":self.zone_set, "zones":self.zones.iter().map(|zone|
|
||||
(zone.width, zone.height, zone.x, zone.y)).collect::<Vec<_>>()}))
|
||||
}
|
||||
|
||||
fn session(&self, id: u64) -> Result<&Session, String> {
|
||||
self.sessions
|
||||
.get(&id)
|
||||
.ok_or("Unknown input capture session".into())
|
||||
self.sessions.get(&id).ok_or("Unknown input capture session".into())
|
||||
}
|
||||
|
||||
pub(crate) fn set_barriers(
|
||||
&mut self,
|
||||
id: u64,
|
||||
zone_set: u32,
|
||||
barriers: Vec<Barrier>,
|
||||
) -> Result<Vec<u32>, String> {
|
||||
pub(crate) fn set_barriers(&mut self, id: u64, zone_set: u32, barriers: Vec<Barrier>) -> Result<Vec<u32>, String> {
|
||||
self.session(id)?;
|
||||
if barriers.len() > MAX_BARRIERS {
|
||||
return Err("Too many pointer barriers".into());
|
||||
@@ -250,10 +226,7 @@ impl InputCapture {
|
||||
}
|
||||
|
||||
pub(crate) fn enable(&mut self, id: u64) -> Result<(), String> {
|
||||
let session = self
|
||||
.sessions
|
||||
.get_mut(&id)
|
||||
.ok_or("Unknown input capture session")?;
|
||||
let session = self.sessions.get_mut(&id).ok_or("Unknown input capture session")?;
|
||||
if session.closed {
|
||||
return Err("Input capture session is closed".into());
|
||||
}
|
||||
@@ -279,11 +252,7 @@ impl InputCapture {
|
||||
if self.active() {
|
||||
return true;
|
||||
}
|
||||
if !allowed
|
||||
|| ![from.0, from.1, to.0, to.1]
|
||||
.iter()
|
||||
.all(|value| value.is_finite())
|
||||
{
|
||||
if !allowed || ![from.0, from.1, to.0, to.1].iter().all(|value| value.is_finite()) {
|
||||
return false;
|
||||
}
|
||||
let hit = self
|
||||
@@ -368,10 +337,7 @@ impl InputCapture {
|
||||
self.restore_focus = true;
|
||||
let session = self.sessions.get_mut(&id).unwrap();
|
||||
let activation = session.activation.take();
|
||||
self.enqueue(
|
||||
id,
|
||||
json!({"type":"deactivated", "activation_id":activation}),
|
||||
);
|
||||
self.enqueue(id, json!({"type":"deactivated", "activation_id":activation}));
|
||||
self.flush();
|
||||
}
|
||||
Ok(())
|
||||
@@ -396,18 +362,10 @@ impl InputCapture {
|
||||
}
|
||||
|
||||
pub(crate) fn valid_position(&self, point: (f64, f64)) -> bool {
|
||||
point.0.is_finite()
|
||||
&& point.1.is_finite()
|
||||
&& self.zones.iter().any(|zone| zone.contains(point))
|
||||
point.0.is_finite() && point.1.is_finite() && self.zones.iter().any(|zone| zone.contains(point))
|
||||
}
|
||||
|
||||
pub(crate) fn watch(
|
||||
&mut self,
|
||||
owner: u64,
|
||||
request: u64,
|
||||
id: u64,
|
||||
response: SyncSender<Response>,
|
||||
) {
|
||||
pub(crate) fn watch(&mut self, owner: u64, request: u64, id: u64, response: SyncSender<Response>) {
|
||||
if !self.sessions.contains_key(&id)
|
||||
|| self.waiters.len() >= MAX_SESSIONS
|
||||
|| self.waiters.values().any(|(_, token, _)| *token == id)
|
||||
@@ -491,10 +449,7 @@ mod tests {
|
||||
[3840, 0, 3840, 1079],
|
||||
[0, 0, 0, 1079],
|
||||
] {
|
||||
assert!(
|
||||
edge(&Barrier { id: 1, position }, &zones).is_some(),
|
||||
"{position:?}"
|
||||
);
|
||||
assert!(edge(&Barrier { id: 1, position }, &zones).is_some(), "{position:?}");
|
||||
}
|
||||
for position in [
|
||||
[1920, 0, 1920, 1079],
|
||||
@@ -503,10 +458,7 @@ mod tests {
|
||||
[50, 50, 50, 200],
|
||||
[0, 5, 0, 4],
|
||||
] {
|
||||
assert!(
|
||||
edge(&Barrier { id: 1, position }, &zones).is_none(),
|
||||
"{position:?}"
|
||||
);
|
||||
assert!(edge(&Barrier { id: 1, position }, &zones).is_none(), "{position:?}");
|
||||
}
|
||||
let barrier = Barrier {
|
||||
id: 1,
|
||||
@@ -595,11 +547,7 @@ mod tests {
|
||||
capture.enable(id).unwrap();
|
||||
assert!(capture.motion((3839.0, 50.0), (3900.0, 50.0), true));
|
||||
let activation = capture.sessions[&id].activation.unwrap();
|
||||
assert!(
|
||||
capture
|
||||
.release(id, Some(activation.wrapping_add(1)))
|
||||
.is_err()
|
||||
);
|
||||
assert!(capture.release(id, Some(activation.wrapping_add(1))).is_err());
|
||||
capture.release(id, Some(activation)).unwrap();
|
||||
assert!(capture.sessions[&id].enabled);
|
||||
capture.update_zones(vec![zones()[0]]);
|
||||
@@ -667,19 +615,10 @@ impl crate::Ferese {
|
||||
&& !self.overview.is_presenting()
|
||||
&& !pointer.is_grabbed()
|
||||
&& !self.swipe.active()
|
||||
&& self
|
||||
.seat
|
||||
.get_keyboard()
|
||||
.is_none_or(|keyboard| !keyboard.is_grabbed())
|
||||
&& self
|
||||
.seat
|
||||
.get_touch()
|
||||
.is_none_or(|touch| !touch.is_grabbed())
|
||||
&& self.seat.get_keyboard().is_none_or(|keyboard| !keyboard.is_grabbed())
|
||||
&& self.seat.get_touch().is_none_or(|touch| !touch.is_grabbed())
|
||||
&& !self.seat.keyboard_shortcuts_inhibited();
|
||||
if !self
|
||||
.input_capture
|
||||
.motion((from.x, from.y), (to.x, to.y), allowed)
|
||||
{
|
||||
if !self.input_capture.motion((from.x, from.y), (to.x, to.y), allowed) {
|
||||
return false;
|
||||
}
|
||||
let delta = if absolute {
|
||||
|
||||
+137
-251
@@ -1,32 +1,26 @@
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::os::fd::AsRawFd;
|
||||
use std::os::unix::fs::{FileTypeExt, MetadataExt, PermissionsExt};
|
||||
use std::os::unix::net::{UnixListener, UnixStream};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
|
||||
use std::sync::{
|
||||
Arc,
|
||||
mpsc::{SyncSender, sync_channel},
|
||||
};
|
||||
use std::thread;
|
||||
use std::sync::mpsc::{SyncSender, sync_channel};
|
||||
use std::time::Duration;
|
||||
use std::{env, fs, io, thread};
|
||||
|
||||
use ferese_core::LayoutMode;
|
||||
use ferese_ipc::{Request, Response, VERSION, read_frame, write_frame};
|
||||
use ferese_layout::Direction;
|
||||
use serde_json::{Value, json};
|
||||
use smithay::output::Output;
|
||||
use smithay::reexports::calloop::{EventLoop, LoopSignal, channel, timer};
|
||||
use smithay::utils::Transform;
|
||||
|
||||
use crate::handlers::screencopy;
|
||||
use crate::handlers::screenshot::{
|
||||
Action, Geometry, OutputLayout, PartOutcome, PartSender, parse_geometry, plan,
|
||||
};
|
||||
use crate::handlers::screenshot_worker::{self, Encoded, Job, Worker};
|
||||
use crate::{Ferese, config::OutputTransform};
|
||||
use smithay::output::Output;
|
||||
use crate::Ferese;
|
||||
use crate::config::OutputTransform;
|
||||
use crate::handlers::screenshot::{Action, Geometry, OutputLayout, PartOutcome, PartSender, parse_geometry, plan};
|
||||
use crate::handlers::screenshot_worker::{Encoded, Job, Worker};
|
||||
use crate::handlers::{screencopy, screenshot_worker};
|
||||
|
||||
const REQUEST_QUEUE_CAPACITY: usize = 128;
|
||||
const MAX_CONNECTIONS: usize = 64;
|
||||
@@ -90,18 +84,14 @@ pub(crate) struct IpcSocketGuard {
|
||||
impl Drop for IpcSocketGuard {
|
||||
fn drop(&mut self) {
|
||||
if self.path.symlink_metadata().is_ok_and(|metadata| {
|
||||
metadata.file_type().is_socket()
|
||||
&& metadata.dev() == self.device
|
||||
&& metadata.ino() == self.inode
|
||||
metadata.file_type().is_socket() && metadata.dev() == self.device && metadata.ino() == self.inode
|
||||
}) {
|
||||
let _ = fs::remove_file(&self.path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn init(
|
||||
event_loop: &mut EventLoop<'static, Ferese>,
|
||||
) -> Result<ScreenshotInit, Box<dyn std::error::Error>> {
|
||||
pub(crate) fn init(event_loop: &mut EventLoop<'static, Ferese>) -> Result<ScreenshotInit, Box<dyn std::error::Error>> {
|
||||
let path = socket_path()?;
|
||||
prepare_parent(&path)?;
|
||||
let listener = bind_listener(&path)?;
|
||||
@@ -115,101 +105,88 @@ pub(crate) fn init(
|
||||
|
||||
let (sender, receiver): (channel::SyncSender<IpcEvent>, channel::Channel<IpcEvent>) =
|
||||
channel::sync_channel(REQUEST_QUEUE_CAPACITY);
|
||||
event_loop
|
||||
.handle()
|
||||
.insert_source(receiver, |event, _, state| {
|
||||
if let channel::Event::Msg(event) = event {
|
||||
let call = match event {
|
||||
IpcEvent::Call(call) => call,
|
||||
IpcEvent::Closed(owner) => {
|
||||
let captured = state.input_capture.active();
|
||||
state.input_capture.remove_owner(owner);
|
||||
if captured && !state.input_capture.active() {
|
||||
state.restore_input_capture_focus();
|
||||
}
|
||||
state.portal_shortcuts.remove(owner);
|
||||
state.portal_session.remove(owner);
|
||||
state.refresh_idle_inhibition();
|
||||
return;
|
||||
event_loop.handle().insert_source(receiver, |event, _, state| {
|
||||
if let channel::Event::Msg(event) = event {
|
||||
let call = match event {
|
||||
IpcEvent::Call(call) => call,
|
||||
IpcEvent::Closed(owner) => {
|
||||
let captured = state.input_capture.active();
|
||||
state.input_capture.remove_owner(owner);
|
||||
if captured && !state.input_capture.active() {
|
||||
state.restore_input_capture_focus();
|
||||
}
|
||||
};
|
||||
if call.request.command.starts_with("input-capture-") && !call.native_portal {
|
||||
state.portal_shortcuts.remove(owner);
|
||||
state.portal_session.remove(owner);
|
||||
state.refresh_idle_inhibition();
|
||||
return;
|
||||
}
|
||||
};
|
||||
if call.request.command.starts_with("input-capture-") && !call.native_portal {
|
||||
let _ = call.response.try_send(Response::error(
|
||||
call.request.id,
|
||||
"access_denied",
|
||||
"Input capture is restricted to the native portal",
|
||||
));
|
||||
} else if call.request.command == "input-capture-watch" {
|
||||
if let Err(error) = validate_request(&call.request) {
|
||||
let _ = call
|
||||
.response
|
||||
.try_send(Response::error(call.request.id, error.code, error.message));
|
||||
} else if let Some(id) = call.request.args["session"].as_u64() {
|
||||
state
|
||||
.input_capture
|
||||
.watch(call.owner, call.request.id, id, call.response);
|
||||
} else {
|
||||
let _ = call.response.try_send(Response::error(
|
||||
call.request.id,
|
||||
"access_denied",
|
||||
"Input capture is restricted to the native portal",
|
||||
"invalid_argument",
|
||||
"Missing capture session",
|
||||
));
|
||||
} else if call.request.command == "input-capture-watch" {
|
||||
if let Err(error) = validate_request(&call.request) {
|
||||
let _ = call.response.try_send(Response::error(
|
||||
call.request.id,
|
||||
error.code,
|
||||
error.message,
|
||||
));
|
||||
} else if let Some(id) = call.request.args["session"].as_u64() {
|
||||
state
|
||||
.input_capture
|
||||
.watch(call.owner, call.request.id, id, call.response);
|
||||
} else {
|
||||
let _ = call.response.try_send(Response::error(
|
||||
call.request.id,
|
||||
"invalid_argument",
|
||||
"Missing capture session",
|
||||
));
|
||||
}
|
||||
} else if call.request.command == "session-watch" {
|
||||
if let Err(error) = validate_request(&call.request) {
|
||||
let _ = call.response.send(Response::error(
|
||||
call.request.id,
|
||||
error.code,
|
||||
error.message,
|
||||
));
|
||||
} else if let Some(since) = call.request.args["since"].as_u64() {
|
||||
state.portal_session.watch(
|
||||
call.owner,
|
||||
call.request.id,
|
||||
since,
|
||||
call.response,
|
||||
);
|
||||
} else {
|
||||
let _ = call.response.send(Response::error(
|
||||
call.request.id,
|
||||
"invalid_argument",
|
||||
"Missing session revision",
|
||||
));
|
||||
}
|
||||
} else if call.request.command == "screenshot-window" {
|
||||
state.start_window_screenshot(call.request, call.response);
|
||||
} else if call.request.command == "screenshot" {
|
||||
// Deferred: this path answers the caller itself, exactly
|
||||
// once, whenever the request finishes or is terminated.
|
||||
state.start_screenshot(call.request, call.response);
|
||||
} else {
|
||||
let response = state.handle_ipc_request(call.owner, call.request);
|
||||
let _ = call.response.send(response);
|
||||
}
|
||||
} else if call.request.command == "session-watch" {
|
||||
if let Err(error) = validate_request(&call.request) {
|
||||
let _ = call
|
||||
.response
|
||||
.send(Response::error(call.request.id, error.code, error.message));
|
||||
} else if let Some(since) = call.request.args["since"].as_u64() {
|
||||
state
|
||||
.portal_session
|
||||
.watch(call.owner, call.request.id, since, call.response);
|
||||
} else {
|
||||
let _ = call.response.send(Response::error(
|
||||
call.request.id,
|
||||
"invalid_argument",
|
||||
"Missing session revision",
|
||||
));
|
||||
}
|
||||
} else if call.request.command == "screenshot-window" {
|
||||
state.start_window_screenshot(call.request, call.response);
|
||||
} else if call.request.command == "screenshot" {
|
||||
// Deferred: this path answers the caller itself, exactly
|
||||
// once, whenever the request finishes or is terminated.
|
||||
state.start_screenshot(call.request, call.response);
|
||||
} else {
|
||||
let response = state.handle_ipc_request(call.owner, call.request);
|
||||
let _ = call.response.send(response);
|
||||
}
|
||||
})?;
|
||||
}
|
||||
})?;
|
||||
|
||||
// Screenshot readbacks publish parts here, so the loop wakes as soon as a
|
||||
// readback lands, whichever backend produced it.
|
||||
let (parts, part_events) = channel::channel::<PartOutcome>();
|
||||
event_loop
|
||||
.handle()
|
||||
.insert_source(part_events, |event, _, state| {
|
||||
if let channel::Event::Msg(outcome) = event {
|
||||
state.on_screenshot_part(outcome);
|
||||
}
|
||||
})?;
|
||||
event_loop.handle().insert_source(part_events, |event, _, state| {
|
||||
if let channel::Event::Msg(outcome) = event {
|
||||
state.on_screenshot_part(outcome);
|
||||
}
|
||||
})?;
|
||||
|
||||
let (encoded, encoded_events) = channel::sync_channel::<Encoded>(RESULT_QUEUE_CAPACITY);
|
||||
event_loop
|
||||
.handle()
|
||||
.insert_source(encoded_events, |event, _, state| {
|
||||
if let channel::Event::Msg(result) = event {
|
||||
state.on_screenshot_encoded(result);
|
||||
}
|
||||
})?;
|
||||
event_loop.handle().insert_source(encoded_events, |event, _, state| {
|
||||
if let channel::Event::Msg(result) = event {
|
||||
state.on_screenshot_encoded(result);
|
||||
}
|
||||
})?;
|
||||
let worker = Worker::spawn(encoded);
|
||||
|
||||
let signal = event_loop.get_signal();
|
||||
@@ -231,9 +208,9 @@ pub(crate) fn init(
|
||||
// Readbacks are published by the render path, so a request whose outputs
|
||||
// are not redrawn would otherwise wait forever. This re-arms to the exact
|
||||
// remaining time whenever a request is outstanding, and idles otherwise.
|
||||
event_loop.handle().insert_source(
|
||||
timer::Timer::from_duration(DEADLINE_IDLE),
|
||||
|_, _, state| {
|
||||
event_loop
|
||||
.handle()
|
||||
.insert_source(timer::Timer::from_duration(DEADLINE_IDLE), |_, _, state| {
|
||||
// Abandoned requests have already been answered, so their
|
||||
// readbacks are simply dropped: they are not failed, because
|
||||
// that would publish into a request that no longer exists.
|
||||
@@ -244,8 +221,7 @@ pub(crate) fn init(
|
||||
}
|
||||
let next = state.screenshot.next_deadline().unwrap_or(DEADLINE_IDLE);
|
||||
timer::TimeoutAction::ToDuration(next)
|
||||
},
|
||||
)?;
|
||||
})?;
|
||||
|
||||
tracing::info!(path = %path.display(), "Ferese IPC is accepting connections");
|
||||
|
||||
@@ -265,11 +241,7 @@ pub(crate) struct ScreenshotInit {
|
||||
pub(crate) _guard: IpcSocketGuard,
|
||||
}
|
||||
|
||||
fn accept_connections(
|
||||
listener: UnixListener,
|
||||
sender: channel::SyncSender<IpcEvent>,
|
||||
signal: LoopSignal,
|
||||
) {
|
||||
fn accept_connections(listener: UnixListener, sender: channel::SyncSender<IpcEvent>, signal: LoopSignal) {
|
||||
let active_connections = Arc::new(AtomicUsize::new(0));
|
||||
let connection_ids = AtomicU64::new(1);
|
||||
|
||||
@@ -296,10 +268,7 @@ fn accept_connections(
|
||||
}
|
||||
|
||||
let Some(permit) = try_acquire_connection(&active_connections) else {
|
||||
tracing::warn!(
|
||||
limit = MAX_CONNECTIONS,
|
||||
"rejected IPC connection at worker limit"
|
||||
);
|
||||
tracing::warn!(limit = MAX_CONNECTIONS, "rejected IPC connection at worker limit");
|
||||
continue;
|
||||
};
|
||||
let sender = sender.clone();
|
||||
@@ -329,17 +298,10 @@ fn try_acquire_connection(active: &Arc<AtomicUsize>) -> Option<ConnectionPermit>
|
||||
}
|
||||
}
|
||||
|
||||
Some(ConnectionPermit {
|
||||
active: active.clone(),
|
||||
})
|
||||
Some(ConnectionPermit { active: active.clone() })
|
||||
}
|
||||
|
||||
fn serve_connection(
|
||||
mut stream: UnixStream,
|
||||
sender: channel::SyncSender<IpcEvent>,
|
||||
signal: LoopSignal,
|
||||
owner: u64,
|
||||
) {
|
||||
fn serve_connection(mut stream: UnixStream, sender: channel::SyncSender<IpcEvent>, signal: LoopSignal, owner: u64) {
|
||||
let native_portal = crate::handlers::window_capture::is_portal(&stream);
|
||||
let _connection = IpcConnection {
|
||||
owner,
|
||||
@@ -351,9 +313,7 @@ fn serve_connection(
|
||||
Err(ferese_ipc::FrameError::Io(error))
|
||||
if matches!(
|
||||
error.kind(),
|
||||
io::ErrorKind::UnexpectedEof
|
||||
| io::ErrorKind::ConnectionReset
|
||||
| io::ErrorKind::BrokenPipe
|
||||
io::ErrorKind::UnexpectedEof | io::ErrorKind::ConnectionReset | io::ErrorKind::BrokenPipe
|
||||
) =>
|
||||
{
|
||||
return;
|
||||
@@ -365,8 +325,7 @@ fn serve_connection(
|
||||
};
|
||||
let (response, receiver) = sync_channel(1);
|
||||
let exit_requested = request.command == "exit";
|
||||
let screenshot_requested =
|
||||
matches!(request.command.as_str(), "screenshot" | "screenshot-window");
|
||||
let screenshot_requested = matches!(request.command.as_str(), "screenshot" | "screenshot-window");
|
||||
let call = IpcEvent::Call(IpcCall {
|
||||
native_portal,
|
||||
owner,
|
||||
@@ -432,10 +391,7 @@ fn u32_arg(args: &Value, key: &str) -> Result<u32, CommandError> {
|
||||
|
||||
fn discard_undelivered_screenshot(response: &Response) {
|
||||
if response.error.is_none()
|
||||
&& let Some(path) = response
|
||||
.result
|
||||
.as_ref()
|
||||
.and_then(|result| result["path"].as_str())
|
||||
&& let Some(path) = response.result.as_ref().and_then(|result| result["path"].as_str())
|
||||
&& let Err(error) = fs::remove_file(path)
|
||||
&& error.kind() != io::ErrorKind::NotFound
|
||||
{
|
||||
@@ -459,11 +415,7 @@ impl Ferese {
|
||||
| "cancel-session-end"
|
||||
)
|
||||
{
|
||||
return Response::error(
|
||||
request.id,
|
||||
"session_locked",
|
||||
"IPC unavailable while session is locked",
|
||||
);
|
||||
return Response::error(request.id, "session_locked", "IPC unavailable while session is locked");
|
||||
}
|
||||
if let Err(error) = validate_request(&request) {
|
||||
return Response::error(request.id, error.code, error.message);
|
||||
@@ -475,12 +427,7 @@ impl Ferese {
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatch_ipc_command(
|
||||
&mut self,
|
||||
owner: u64,
|
||||
command: &str,
|
||||
args: &Value,
|
||||
) -> Result<Value, CommandError> {
|
||||
fn dispatch_ipc_command(&mut self, owner: u64, command: &str, args: &Value) -> Result<Value, CommandError> {
|
||||
match command {
|
||||
"portal-inhibit" => {
|
||||
let inhibition = serde_json::from_value(args.clone())
|
||||
@@ -522,9 +469,9 @@ impl Ferese {
|
||||
.validate_end(
|
||||
u32_arg(args, "token")?,
|
||||
u32_arg(args, "inhibitor-revision")?,
|
||||
args["force"].as_bool().ok_or_else(|| {
|
||||
CommandError::new("invalid_argument", "Missing force confirmation")
|
||||
})?,
|
||||
args["force"]
|
||||
.as_bool()
|
||||
.ok_or_else(|| CommandError::new("invalid_argument", "Missing force confirmation"))?,
|
||||
)
|
||||
.map_err(|error| CommandError::new("inhibited", error))?;
|
||||
return Ok(json!({}));
|
||||
@@ -534,17 +481,17 @@ impl Ferese {
|
||||
.commit_end(
|
||||
u32_arg(args, "token")?,
|
||||
u32_arg(args, "inhibitor-revision")?,
|
||||
args["force"].as_bool().ok_or_else(|| {
|
||||
CommandError::new("invalid_argument", "Missing force confirmation")
|
||||
})?,
|
||||
args["force"]
|
||||
.as_bool()
|
||||
.ok_or_else(|| CommandError::new("invalid_argument", "Missing force confirmation"))?,
|
||||
)
|
||||
.map_err(|error| CommandError::new("inhibited", error))?;
|
||||
return Ok(json!({}));
|
||||
}
|
||||
"logind-session-ending" => {
|
||||
let ending = args["ending"].as_bool().ok_or_else(|| {
|
||||
CommandError::new("invalid_argument", "Missing shutdown state")
|
||||
})?;
|
||||
let ending = args["ending"]
|
||||
.as_bool()
|
||||
.ok_or_else(|| CommandError::new("invalid_argument", "Missing shutdown state"))?;
|
||||
self.portal_session.set_phase(if ending { 3 } else { 1 });
|
||||
return Ok(json!({}));
|
||||
}
|
||||
@@ -573,8 +520,7 @@ impl Ferese {
|
||||
let xkb = state.xkb().lock().unwrap();
|
||||
// SAFETY: the ref-counted keymap does not escape the locked Xkb;
|
||||
// only its owned string is retained for the EIS keyboard.
|
||||
unsafe { xkb.keymap() }
|
||||
.get_as_string(smithay::input::keyboard::xkb::KEYMAP_FORMAT_TEXT_V1)
|
||||
unsafe { xkb.keymap() }.get_as_string(smithay::input::keyboard::xkb::KEYMAP_FORMAT_TEXT_V1)
|
||||
});
|
||||
return Ok(json!({"session":id, "keymap":keymap}));
|
||||
}
|
||||
@@ -583,9 +529,9 @@ impl Ferese {
|
||||
return self
|
||||
.input_capture
|
||||
.zones(
|
||||
args["session"].as_u64().ok_or_else(|| {
|
||||
CommandError::new("invalid_capture", "Missing session")
|
||||
})?,
|
||||
args["session"]
|
||||
.as_u64()
|
||||
.ok_or_else(|| CommandError::new("invalid_capture", "Missing session"))?,
|
||||
)
|
||||
.map_err(|e| CommandError::new("invalid_capture", e));
|
||||
}
|
||||
@@ -642,12 +588,8 @@ impl Ferese {
|
||||
let result = if command == "input-capture-disable" {
|
||||
self.input_capture.disable(id)
|
||||
} else {
|
||||
self.input_capture.release(
|
||||
id,
|
||||
args["activation_id"]
|
||||
.as_u64()
|
||||
.and_then(|id| u32::try_from(id).ok()),
|
||||
)
|
||||
self.input_capture
|
||||
.release(id, args["activation_id"].as_u64().and_then(|id| u32::try_from(id).ok()))
|
||||
};
|
||||
result.map_err(|e| CommandError::new("invalid_capture", e))?;
|
||||
if active && !self.input_capture.active() {
|
||||
@@ -696,9 +638,7 @@ impl Ferese {
|
||||
.get("pid")
|
||||
.and_then(Value::as_u64)
|
||||
.and_then(|pid| u32::try_from(pid).ok())
|
||||
.ok_or_else(|| {
|
||||
CommandError::new("invalid_argument", "pid must be an unsigned process ID")
|
||||
})?;
|
||||
.ok_or_else(|| CommandError::new("invalid_argument", "pid must be an unsigned process ID"))?;
|
||||
return Ok(json!(self.has_client_surfaces(pid)));
|
||||
}
|
||||
"get-workspaces" => return Ok(self.workspaces_json()),
|
||||
@@ -783,10 +723,7 @@ impl Ferese {
|
||||
return;
|
||||
}
|
||||
};
|
||||
let specs = planned
|
||||
.iter()
|
||||
.map(|part| part.spec(&layouts[part.index]))
|
||||
.collect();
|
||||
let specs = planned.iter().map(|part| part.spec(&layouts[part.index])).collect();
|
||||
|
||||
// admit stores the reply only on success, so a clone survives the
|
||||
// rejection path and every caller is answered exactly once.
|
||||
@@ -794,8 +731,7 @@ impl Ferese {
|
||||
let id = match self.screenshot.admit(request.id, response, specs) {
|
||||
Ok(id) => id,
|
||||
Err(message) => {
|
||||
let _ =
|
||||
fallback.try_send(Response::error(request.id, "screenshot_rejected", message));
|
||||
let _ = fallback.try_send(Response::error(request.id, "screenshot_rejected", message));
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -819,11 +755,7 @@ impl Ferese {
|
||||
|
||||
fn start_window_screenshot(&mut self, request: Request, response: SyncSender<Response>) {
|
||||
let reject = |message: String| {
|
||||
let _ = response.try_send(Response::error(
|
||||
request.id,
|
||||
"window_capture_failed",
|
||||
message,
|
||||
));
|
||||
let _ = response.try_send(Response::error(request.id, "window_capture_failed", message));
|
||||
};
|
||||
if self.session_lock.active || !screencopy::capture_allowed() {
|
||||
reject("Screen capture is unavailable".into());
|
||||
@@ -877,19 +809,13 @@ impl Ferese {
|
||||
let capture = match self.screenshot.admit(request.id, response, vec![spec]) {
|
||||
Ok(capture) => capture,
|
||||
Err(error) => {
|
||||
let _ =
|
||||
fallback.try_send(Response::error(request.id, "screenshot_rejected", error));
|
||||
let _ = fallback.try_send(Response::error(request.id, "screenshot_rejected", error));
|
||||
return;
|
||||
}
|
||||
};
|
||||
let result = if let Some(backend) = &self.nested_backend {
|
||||
match backend.try_borrow_mut() {
|
||||
Ok(mut backend) => crate::winit::capture_window_buffer(
|
||||
backend.renderer(),
|
||||
&window,
|
||||
geometry,
|
||||
scale,
|
||||
),
|
||||
Ok(mut backend) => crate::winit::capture_window_buffer(backend.renderer(), &window, geometry, scale),
|
||||
Err(_) => Err("Window renderer is busy".into()),
|
||||
}
|
||||
} else if let Some(backend) = &mut self.direct_backend {
|
||||
@@ -905,9 +831,7 @@ impl Ferese {
|
||||
}
|
||||
|
||||
pub(crate) fn on_screenshot_part(&mut self, outcome: PartOutcome) {
|
||||
let action = self
|
||||
.screenshot
|
||||
.on_part(outcome.request, outcome.part, outcome.result);
|
||||
let action = self.screenshot.on_part(outcome.request, outcome.part, outcome.result);
|
||||
self.apply_screenshot_action(action);
|
||||
}
|
||||
|
||||
@@ -925,8 +849,7 @@ impl Ferese {
|
||||
.as_ref()
|
||||
.is_some_and(|worker| worker.submit(Job { request, frames }).is_ok());
|
||||
if !submitted {
|
||||
self.screenshot
|
||||
.reject(request, "Screenshot encoding queue is full");
|
||||
self.screenshot.reject(request, "Screenshot encoding queue is full");
|
||||
}
|
||||
}
|
||||
Action::Delivered(path) => {
|
||||
@@ -955,13 +878,12 @@ impl Ferese {
|
||||
|
||||
fn has_client_surfaces(&self, pid: u32) -> bool {
|
||||
use smithay::reexports::wayland_server::Resource;
|
||||
let belongs_to =
|
||||
|surface: &smithay::reexports::wayland_server::protocol::wl_surface::WlSurface| {
|
||||
surface
|
||||
.client()
|
||||
.and_then(|client| client.get_credentials(&self.display_handle).ok())
|
||||
.is_some_and(|credentials| credentials.pid as u32 == pid)
|
||||
};
|
||||
let belongs_to = |surface: &smithay::reexports::wayland_server::protocol::wl_surface::WlSurface| {
|
||||
surface
|
||||
.client()
|
||||
.and_then(|client| client.get_credentials(&self.display_handle).ok())
|
||||
.is_some_and(|credentials| credentials.pid as u32 == pid)
|
||||
};
|
||||
self.window_ids
|
||||
.keys()
|
||||
.filter_map(|window| window.toplevel())
|
||||
@@ -974,19 +896,15 @@ impl Ferese {
|
||||
}
|
||||
|
||||
fn windows_json(&self) -> Value {
|
||||
use smithay::wayland::{compositor::with_states, shell::xdg::XdgToplevelSurfaceData};
|
||||
use smithay::wayland::compositor::with_states;
|
||||
use smithay::wayland::shell::xdg::XdgToplevelSurfaceData;
|
||||
let mut windows = self
|
||||
.window_ids
|
||||
.iter()
|
||||
.filter_map(|(window, id)| {
|
||||
let toplevel = window.toplevel()?;
|
||||
let (app_id, title) = with_states(toplevel.wl_surface(), |states| {
|
||||
let attributes = states
|
||||
.data_map
|
||||
.get::<XdgToplevelSurfaceData>()
|
||||
.unwrap()
|
||||
.lock()
|
||||
.unwrap();
|
||||
let attributes = states.data_map.get::<XdgToplevelSurfaceData>().unwrap().lock().unwrap();
|
||||
(
|
||||
attributes.app_id.clone().unwrap_or_default(),
|
||||
attributes.title.clone().unwrap_or_default(),
|
||||
@@ -1044,10 +962,7 @@ impl Ferese {
|
||||
.connected_outputs
|
||||
.iter()
|
||||
.map(|info| {
|
||||
let mapped = self
|
||||
.space
|
||||
.outputs()
|
||||
.find(|output| output.name() == info.connector);
|
||||
let mapped = self.space.outputs().find(|output| output.name() == info.connector);
|
||||
let id = mapped.and_then(|output| self.output_id(output));
|
||||
let geometry = mapped.and_then(|output| self.space.output_geometry(output));
|
||||
let workspace = id.and_then(|id| self.output_workspaces.active_workspace(id));
|
||||
@@ -1105,9 +1020,7 @@ impl Ferese {
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
outputs.sort_by(|left, right| {
|
||||
left["connector"].as_str().cmp(&right["connector"].as_str())
|
||||
});
|
||||
outputs.sort_by(|left, right| left["connector"].as_str().cmp(&right["connector"].as_str()));
|
||||
return Value::Array(outputs);
|
||||
}
|
||||
|
||||
@@ -1163,9 +1076,7 @@ fn geometry_arg(args: &Value) -> Result<Geometry, String> {
|
||||
match args.get("geometry") {
|
||||
None | Some(Value::Null) => Ok(Geometry::All),
|
||||
Some(value) => {
|
||||
let text = value
|
||||
.as_str()
|
||||
.ok_or("geometry must be a string like \"x,y WxH\"")?;
|
||||
let text = value.as_str().ok_or("geometry must be a string like \"x,y WxH\"")?;
|
||||
parse_geometry(text)
|
||||
}
|
||||
}
|
||||
@@ -1179,10 +1090,7 @@ fn validate_request(request: &Request) -> Result<(), CommandError> {
|
||||
));
|
||||
}
|
||||
if request.kind != "command" {
|
||||
return Err(CommandError::new(
|
||||
"invalid_type",
|
||||
"expected type \"command\"",
|
||||
));
|
||||
return Err(CommandError::new("invalid_type", "expected type \"command\""));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -1247,12 +1155,7 @@ fn workspace_arg(args: &Value) -> Result<u32, CommandError> {
|
||||
.and_then(Value::as_u64)
|
||||
.and_then(|index| u32::try_from(index).ok())
|
||||
.filter(|index| *index > 0)
|
||||
.ok_or_else(|| {
|
||||
CommandError::new(
|
||||
"invalid_argument",
|
||||
"index must be a positive 32-bit integer",
|
||||
)
|
||||
})
|
||||
.ok_or_else(|| CommandError::new("invalid_argument", "index must be a positive 32-bit integer"))
|
||||
}
|
||||
|
||||
fn socket_path() -> Result<PathBuf, io::Error> {
|
||||
@@ -1264,9 +1167,7 @@ fn socket_path() -> Result<PathBuf, io::Error> {
|
||||
}
|
||||
|
||||
fn prepare_parent(socket: &Path) -> Result<(), io::Error> {
|
||||
let parent = socket
|
||||
.parent()
|
||||
.expect("the Ferese control socket always has a parent");
|
||||
let parent = socket.parent().expect("the Ferese control socket always has a parent");
|
||||
match fs::symlink_metadata(parent) {
|
||||
Ok(metadata) => {
|
||||
if !metadata.is_dir() || metadata.file_type().is_symlink() {
|
||||
@@ -1311,11 +1212,7 @@ fn bind_listener(path: &Path) -> Result<UnixListener, io::Error> {
|
||||
}
|
||||
|
||||
fn peer_uid(stream: &UnixStream) -> Result<u32, io::Error> {
|
||||
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::<libc::ucred>() as libc::socklen_t;
|
||||
|
||||
// SAFETY: `credentials` and `length` point to initialized, correctly sized
|
||||
@@ -1361,10 +1258,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn parses_direction_arguments() {
|
||||
assert_eq!(
|
||||
direction_arg(&json!({ "direction": "left" })).unwrap(),
|
||||
Direction::Left
|
||||
);
|
||||
assert_eq!(direction_arg(&json!({ "direction": "left" })).unwrap(), Direction::Left);
|
||||
assert!(direction_arg(&json!({ "direction": "diagonal" })).is_err());
|
||||
}
|
||||
|
||||
@@ -1402,9 +1296,7 @@ mod tests {
|
||||
let worker = thread::spawn(move || serve_connection(server, sender, signal, 1));
|
||||
let invalid = b"not-json";
|
||||
|
||||
client
|
||||
.write_all(&(invalid.len() as u32).to_be_bytes())
|
||||
.unwrap();
|
||||
client.write_all(&(invalid.len() as u32).to_be_bytes()).unwrap();
|
||||
client.write_all(invalid).unwrap();
|
||||
client.shutdown(Shutdown::Write).unwrap();
|
||||
client
|
||||
@@ -1451,10 +1343,7 @@ mod tests {
|
||||
let socket = root.join("control.sock");
|
||||
let first = bind_listener(&socket).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
bind_listener(&socket).unwrap_err().kind(),
|
||||
io::ErrorKind::AddrInUse
|
||||
);
|
||||
assert_eq!(bind_listener(&socket).unwrap_err().kind(), io::ErrorKind::AddrInUse);
|
||||
drop(first);
|
||||
let replacement = bind_listener(&socket).unwrap();
|
||||
|
||||
@@ -1470,10 +1359,7 @@ mod tests {
|
||||
let socket = root.join("control.sock");
|
||||
fs::write(&socket, b"keep").unwrap();
|
||||
|
||||
assert_eq!(
|
||||
bind_listener(&socket).unwrap_err().kind(),
|
||||
io::ErrorKind::AddrInUse
|
||||
);
|
||||
assert_eq!(bind_listener(&socket).unwrap_err().kind(), io::ErrorKind::AddrInUse);
|
||||
assert_eq!(fs::read(&socket).unwrap(), b"keep");
|
||||
fs::remove_dir_all(root).unwrap();
|
||||
}
|
||||
|
||||
+20
-28
@@ -28,16 +28,14 @@ mod wallpaper;
|
||||
mod window_rules;
|
||||
mod winit;
|
||||
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
process::{Child, Command},
|
||||
thread,
|
||||
time::Duration,
|
||||
};
|
||||
use std::error::Error;
|
||||
use std::process::{Child, Command};
|
||||
use std::time::Duration;
|
||||
use std::{io, thread};
|
||||
|
||||
use calloop::signals::{Signal, Signals};
|
||||
use smithay::reexports::{calloop::EventLoop, wayland_server::Display};
|
||||
use smithay::reexports::calloop::EventLoop;
|
||||
use smithay::reexports::wayland_server::Display;
|
||||
pub use state::{Ferese, RuntimeConfig};
|
||||
use tracing::{info, warn};
|
||||
|
||||
@@ -60,8 +58,8 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let launch = LaunchConfig::from_environment()
|
||||
.map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, error))?;
|
||||
let launch =
|
||||
LaunchConfig::from_environment().map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, error))?;
|
||||
let initial_source = config::config_path().and_then(|path| std::fs::read_to_string(path).ok());
|
||||
let config = match initial_source.as_deref() {
|
||||
Some(source) => Config::parse_source(source)?,
|
||||
@@ -81,15 +79,15 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
state.config_source = initial_source.filter(|source| source.len() <= 60 * 1024);
|
||||
overview::init_font_loader(&mut event_loop, &mut state)?;
|
||||
backends::init(launch.backend, &mut event_loop, &mut state)?;
|
||||
let mut monitor = config::config_path().and_then(
|
||||
|path| match reload::ConfigMonitor::with_wakeup(path, Some(event_loop.get_signal())) {
|
||||
let mut monitor = config::config_path().and_then(|path| {
|
||||
match reload::ConfigMonitor::with_wakeup(path, Some(event_loop.get_signal())) {
|
||||
Ok(monitor) => Some(monitor),
|
||||
Err(error) => {
|
||||
warn!(%error, "automatic config watching unavailable; use feresectl reload-config");
|
||||
None
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
use calloop::timer::{TimeoutAction, Timer};
|
||||
info!(socket = ?state.socket_name, backend = ?launch.backend, "Ferese is accepting Wayland clients");
|
||||
let mut child = spawn_client(&mut state, launch.client, launch.client_capabilities);
|
||||
@@ -141,9 +139,9 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
if state.input_capture.restore_focus {
|
||||
state.restore_input_capture_focus();
|
||||
}
|
||||
state.apply_unlocked_pointer_hint();
|
||||
let wallpaper_retry = state.wallpaper.take_retry_wakeup();
|
||||
if std::mem::take(&mut state.cursor_redraw_pending) || wallpaper_changed || wallpaper_retry
|
||||
{
|
||||
if std::mem::take(&mut state.cursor_redraw_pending) || wallpaper_changed || wallpaper_retry {
|
||||
backends::direct::render_all(state);
|
||||
}
|
||||
if let Err(error) = state
|
||||
@@ -242,12 +240,11 @@ fn terminate_child(child: &mut Child) {
|
||||
|
||||
#[cfg(test)]
|
||||
mod startup_tests {
|
||||
use std::io::{Read, Write};
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::*;
|
||||
use std::{
|
||||
io::{Read, Write},
|
||||
os::unix::net::UnixStream,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn deferred_cursor_redraw_runs_after_input_lock_is_released() {
|
||||
@@ -295,15 +292,10 @@ mod startup_tests {
|
||||
}
|
||||
display.dispatch_clients(&mut ()).unwrap();
|
||||
display.handle().flush_clients().unwrap();
|
||||
client
|
||||
.set_read_timeout(Some(Duration::from_secs(1)))
|
||||
.unwrap();
|
||||
client.set_read_timeout(Some(Duration::from_secs(1))).unwrap();
|
||||
let mut reply = [0u8; 12];
|
||||
client.read_exact(&mut reply).unwrap();
|
||||
assert_eq!(u32::from_ne_bytes(reply[0..4].try_into().unwrap()), 2);
|
||||
assert_eq!(
|
||||
u32::from_ne_bytes(reply[4..8].try_into().unwrap()) & 0xffff,
|
||||
0
|
||||
);
|
||||
assert_eq!(u32::from_ne_bytes(reply[4..8].try_into().unwrap()) & 0xffff, 0);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use std::collections::VecDeque;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use smithay::utils::{Physical, Rectangle};
|
||||
|
||||
@@ -94,14 +92,8 @@ impl RenderMetrics {
|
||||
.unwrap_or(0);
|
||||
self.frame_times.make_contiguous().sort_unstable();
|
||||
let percentile = |percent: usize| {
|
||||
let index = (self.frame_times.len() * percent)
|
||||
.div_ceil(100)
|
||||
.saturating_sub(1);
|
||||
self.frame_times
|
||||
.get(index)
|
||||
.copied()
|
||||
.unwrap_or_default()
|
||||
.as_micros()
|
||||
let index = (self.frame_times.len() * percent).div_ceil(100).saturating_sub(1);
|
||||
self.frame_times.get(index).copied().unwrap_or_default().as_micros()
|
||||
};
|
||||
tracing::info!(
|
||||
target: "ferese::render",
|
||||
@@ -131,8 +123,7 @@ impl RenderMetrics {
|
||||
}
|
||||
|
||||
fn rectangle_area(rectangle: &Rectangle<i32, Physical>) -> u64 {
|
||||
u64::try_from(rectangle.size.w.max(0)).unwrap_or(0)
|
||||
* u64::try_from(rectangle.size.h.max(0)).unwrap_or(0)
|
||||
u64::try_from(rectangle.size.w.max(0)).unwrap_or(0) * u64::try_from(rectangle.size.h.max(0)).unwrap_or(0)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -211,14 +202,8 @@ mod tests {
|
||||
for micros in 0..600 {
|
||||
metrics.record_frame(Duration::from_micros(micros), &[], 0, FrameEffectMetrics);
|
||||
}
|
||||
assert_eq!(
|
||||
metrics.frame_times.front(),
|
||||
Some(&Duration::from_micros(88))
|
||||
);
|
||||
assert_eq!(
|
||||
metrics.frame_times.back(),
|
||||
Some(&Duration::from_micros(599))
|
||||
);
|
||||
assert_eq!(metrics.frame_times.front(), Some(&Duration::from_micros(88)));
|
||||
assert_eq!(metrics.frame_times.back(), Some(&Duration::from_micros(599)));
|
||||
assert!(
|
||||
metrics
|
||||
.frame_times
|
||||
|
||||
+53
-158
@@ -1,23 +1,17 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{Arc, Mutex, mpsc},
|
||||
time::Duration,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex, mpsc};
|
||||
use std::time::Duration;
|
||||
|
||||
use ab_glyph::{Font, FontArc, FontVec, ScaleFont};
|
||||
use ferese_animation::{AnimatedRect, AnimatedValue, SpringConfig};
|
||||
use ferese_core::{OutputId, WorkspaceId};
|
||||
use ferese_layout::{Direction, Rect, WindowId};
|
||||
use smithay::{
|
||||
output::Output,
|
||||
utils::{Logical, Point},
|
||||
};
|
||||
use smithay::backend::allocator::Fourcc;
|
||||
use smithay::backend::renderer::element::memory::MemoryRenderBuffer;
|
||||
use smithay::output::Output;
|
||||
use smithay::utils::{Logical, Point, Transform};
|
||||
|
||||
use crate::Ferese;
|
||||
use ab_glyph::{Font, FontArc, FontVec, ScaleFont};
|
||||
use smithay::{
|
||||
backend::{allocator::Fourcc, renderer::element::memory::MemoryRenderBuffer},
|
||||
utils::Transform,
|
||||
};
|
||||
|
||||
const OVERVIEW_MARGIN: f64 = 48.0;
|
||||
const OVERVIEW_GAP: f64 = 40.0;
|
||||
@@ -46,16 +40,14 @@ pub(crate) fn init_font_loader(
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
use smithay::reexports::calloop::channel;
|
||||
let (results, source) = channel::sync_channel::<(String, Option<FontArc>)>(1);
|
||||
event_loop
|
||||
.handle()
|
||||
.insert_source(source, |event, _, state| {
|
||||
if let channel::Event::Msg((family, font)) = event
|
||||
&& state.overview.complete_font_load(&family, font)
|
||||
&& state.overview.is_presenting()
|
||||
{
|
||||
crate::backends::direct::render_all(state);
|
||||
}
|
||||
})?;
|
||||
event_loop.handle().insert_source(source, |event, _, state| {
|
||||
if let channel::Event::Msg((family, font)) = event
|
||||
&& state.overview.complete_font_load(&family, font)
|
||||
&& state.overview.is_presenting()
|
||||
{
|
||||
crate::backends::direct::render_all(state);
|
||||
}
|
||||
})?;
|
||||
let latest = Arc::new(Mutex::new(state.overview.font_family.clone()));
|
||||
let worker_latest = latest.clone();
|
||||
let (wake, requests) = mpsc::sync_channel(1);
|
||||
@@ -92,13 +84,8 @@ pub(crate) fn init_font_loader(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
type OverviewStateLabel = HashMap<
|
||||
(String, u64, [u32; 4]),
|
||||
(
|
||||
MemoryRenderBuffer,
|
||||
smithay::utils::Size<i32, smithay::utils::Buffer>,
|
||||
),
|
||||
>;
|
||||
type OverviewStateLabel =
|
||||
HashMap<(String, u64, [u32; 4]), (MemoryRenderBuffer, smithay::utils::Size<i32, smithay::utils::Buffer>)>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct OverviewState {
|
||||
@@ -154,19 +141,11 @@ pub(crate) fn workspace_strip(bounds: Rect) -> Rect {
|
||||
fn window_area(bounds: Rect) -> Rect {
|
||||
let strip = workspace_strip(bounds);
|
||||
let top = strip.y + strip.height + 16.0;
|
||||
Rect::new(
|
||||
bounds.x,
|
||||
top,
|
||||
bounds.width,
|
||||
(bounds.y + bounds.height - top).max(1.0),
|
||||
)
|
||||
Rect::new(bounds.x, top, bounds.width, (bounds.y + bounds.height - top).max(1.0))
|
||||
}
|
||||
|
||||
fn contains(rect: Rect, point: Point<f64, Logical>) -> bool {
|
||||
point.x >= rect.x
|
||||
&& point.x < rect.x + rect.width
|
||||
&& point.y >= rect.y
|
||||
&& point.y < rect.y + rect.height
|
||||
point.x >= rect.x && point.x < rect.x + rect.width && point.y >= rect.y && point.y < rect.y + rect.height
|
||||
}
|
||||
|
||||
fn strip_capacity(strip: Rect) -> usize {
|
||||
@@ -212,10 +191,7 @@ impl OverviewState {
|
||||
text: &str,
|
||||
scale: f64,
|
||||
color: [f32; 4],
|
||||
) -> Option<(
|
||||
MemoryRenderBuffer,
|
||||
smithay::utils::Size<i32, smithay::utils::Buffer>,
|
||||
)> {
|
||||
) -> Option<(MemoryRenderBuffer, smithay::utils::Size<i32, smithay::utils::Buffer>)> {
|
||||
let key = (text.to_owned(), scale.to_bits(), color.map(f32::to_bits));
|
||||
if let Some(buffer) = self.labels.get(&key) {
|
||||
return Some(buffer.clone());
|
||||
@@ -234,10 +210,7 @@ impl OverviewState {
|
||||
let mut pen = 2.0;
|
||||
for ch in text.chars() {
|
||||
let id = font.glyph_id(ch);
|
||||
let glyph = id.with_scale_and_position(
|
||||
scaled.scale(),
|
||||
ab_glyph::point(pen, scaled.ascent() + 1.0),
|
||||
);
|
||||
let glyph = id.with_scale_and_position(scaled.scale(), ab_glyph::point(pen, scaled.ascent() + 1.0));
|
||||
pen += scaled.h_advance(id);
|
||||
if let Some(outline) = font.outline_glyph(glyph) {
|
||||
let bounds = outline.px_bounds();
|
||||
@@ -257,14 +230,8 @@ impl OverviewState {
|
||||
});
|
||||
}
|
||||
}
|
||||
let buffer = MemoryRenderBuffer::from_slice(
|
||||
&pixels,
|
||||
Fourcc::Abgr8888,
|
||||
(width, height),
|
||||
1,
|
||||
Transform::Normal,
|
||||
None,
|
||||
);
|
||||
let buffer =
|
||||
MemoryRenderBuffer::from_slice(&pixels, Fourcc::Abgr8888, (width, height), 1, Transform::Normal, None);
|
||||
if self.labels.len() >= 128 {
|
||||
self.labels.clear();
|
||||
}
|
||||
@@ -291,13 +258,7 @@ impl OverviewState {
|
||||
.selected
|
||||
.filter(visible)
|
||||
.or_else(|| preferred.filter(visible))
|
||||
.or_else(|| {
|
||||
windows
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(visible)
|
||||
.min_by_key(|id| id.0)
|
||||
});
|
||||
.or_else(|| windows.iter().copied().filter(visible).min_by_key(|id| id.0));
|
||||
}
|
||||
|
||||
pub(crate) fn selected(&self) -> Option<WindowId> {
|
||||
@@ -319,13 +280,9 @@ impl OverviewState {
|
||||
.filter(|(id, _)| Some(**id) != self.selected)
|
||||
.filter_map(|(id, presentation)| {
|
||||
let center = rect_center(presentation.current);
|
||||
directional_distance(direction, current_center, center)
|
||||
.map(|distance| (*id, distance))
|
||||
})
|
||||
.min_by(|(left_id, left), (right_id, right)| {
|
||||
left.total_cmp(right)
|
||||
.then_with(|| left_id.0.cmp(&right_id.0))
|
||||
directional_distance(direction, current_center, center).map(|distance| (*id, distance))
|
||||
})
|
||||
.min_by(|(left_id, left), (right_id, right)| left.total_cmp(right).then_with(|| left_id.0.cmp(&right_id.0)))
|
||||
.map(|(id, _)| id);
|
||||
let Some(next) = next else {
|
||||
return false;
|
||||
@@ -396,10 +353,7 @@ impl OverviewState {
|
||||
presentation.snap();
|
||||
}
|
||||
}
|
||||
if self
|
||||
.selected
|
||||
.is_none_or(|id| !self.presentations.contains_key(&id))
|
||||
{
|
||||
if self.selected.is_none_or(|id| !self.presentations.contains_key(&id)) {
|
||||
self.selected = self.presentations.keys().copied().min_by_key(|id| id.0);
|
||||
}
|
||||
}
|
||||
@@ -429,12 +383,7 @@ impl OverviewState {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn advance(
|
||||
&mut self,
|
||||
delta: Duration,
|
||||
spring: SpringConfig,
|
||||
animations_enabled: bool,
|
||||
) -> bool {
|
||||
pub(crate) fn advance(&mut self, delta: Duration, spring: SpringConfig, animations_enabled: bool) -> bool {
|
||||
if let Some((elapsed, initial_opacity)) = self.exit_transition {
|
||||
let elapsed = if animations_enabled {
|
||||
elapsed + delta.as_secs_f64()
|
||||
@@ -506,18 +455,12 @@ impl Ferese {
|
||||
window: &smithay::desktop::Window,
|
||||
scale: f64,
|
||||
width: f64,
|
||||
) -> Option<(
|
||||
MemoryRenderBuffer,
|
||||
smithay::utils::Size<i32, smithay::utils::Buffer>,
|
||||
)> {
|
||||
use smithay::wayland::{compositor::with_states, shell::xdg::XdgToplevelSurfaceData};
|
||||
) -> Option<(MemoryRenderBuffer, smithay::utils::Size<i32, smithay::utils::Buffer>)> {
|
||||
use smithay::wayland::compositor::with_states;
|
||||
use smithay::wayland::shell::xdg::XdgToplevelSurfaceData;
|
||||
let surface = window.toplevel()?.wl_surface();
|
||||
let title = with_states(surface, |states| {
|
||||
let data = states
|
||||
.data_map
|
||||
.get::<XdgToplevelSurfaceData>()?
|
||||
.lock()
|
||||
.ok()?;
|
||||
let data = states.data_map.get::<XdgToplevelSurfaceData>()?.lock().ok()?;
|
||||
data.title
|
||||
.clone()
|
||||
.filter(|title| !title.trim().is_empty())
|
||||
@@ -539,10 +482,7 @@ impl Ferese {
|
||||
&mut self,
|
||||
workspace: WorkspaceId,
|
||||
scale: f64,
|
||||
) -> Option<(
|
||||
MemoryRenderBuffer,
|
||||
smithay::utils::Size<i32, smithay::utils::Buffer>,
|
||||
)> {
|
||||
) -> Option<(MemoryRenderBuffer, smithay::utils::Size<i32, smithay::utils::Buffer>)> {
|
||||
let name = self.workspaces.workspace(workspace)?.name.clone();
|
||||
self.overview
|
||||
.label(&name, scale, self.theme_settings.text_primary_color.0)
|
||||
@@ -621,8 +561,7 @@ impl Ferese {
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
self.overview
|
||||
.select_workspace_windows(&windows, self.focused_window);
|
||||
self.overview.select_workspace_windows(&windows, self.focused_window);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -637,12 +576,7 @@ impl Ferese {
|
||||
Some(presented)
|
||||
}
|
||||
|
||||
pub(crate) fn inverse_presented_window_point(
|
||||
&self,
|
||||
id: WindowId,
|
||||
x: f64,
|
||||
y: f64,
|
||||
) -> Option<(f64, f64)> {
|
||||
pub(crate) fn inverse_presented_window_point(&self, id: WindowId, x: f64, y: f64) -> Option<(f64, f64)> {
|
||||
let geometry = self.window_geometry.get(&id)?;
|
||||
if !self.overview.is_presenting() {
|
||||
let presented = self.presented_window_rect(id)?;
|
||||
@@ -651,11 +585,7 @@ impl Ferese {
|
||||
let source = geometry.client.committed_size?;
|
||||
let presented = self.overview.presented_rect(id, geometry.visual.current);
|
||||
|
||||
if source.width <= 0
|
||||
|| source.height <= 0
|
||||
|| presented.width <= 0.0
|
||||
|| presented.height <= 0.0
|
||||
{
|
||||
if source.width <= 0 || source.height <= 0 || presented.width <= 0.0 || presented.height <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -692,10 +622,9 @@ impl Ferese {
|
||||
.copied()
|
||||
.unwrap_or_else(|| selected_index.saturating_sub(capacity - 1))
|
||||
.min(max_offset);
|
||||
let card_width =
|
||||
((strip.width - 24.0 - WORKSPACE_CARD_GAP * capacity.saturating_sub(1) as f64)
|
||||
/ capacity as f64)
|
||||
.clamp(1.0, 160.0);
|
||||
let card_width = ((strip.width - 24.0 - WORKSPACE_CARD_GAP * capacity.saturating_sub(1) as f64)
|
||||
/ capacity as f64)
|
||||
.clamp(1.0, 160.0);
|
||||
let visible_count = workspaces.len().saturating_sub(offset).min(capacity);
|
||||
let start_x = centered_row_start(strip, visible_count, card_width);
|
||||
workspaces
|
||||
@@ -726,9 +655,7 @@ impl Ferese {
|
||||
(rect.width - 12.0).max(1.0),
|
||||
(rect.height - 26.0).max(1.0),
|
||||
);
|
||||
let windows = preview_layout(preview_bounds, &windows, 4.0, 4.0)
|
||||
.into_iter()
|
||||
.collect();
|
||||
let windows = preview_layout(preview_bounds, &windows, 4.0, 4.0).into_iter().collect();
|
||||
WorkspaceCard {
|
||||
workspace: workspace.id,
|
||||
rect,
|
||||
@@ -796,11 +723,7 @@ impl Ferese {
|
||||
let ordered = self.workspaces.ordered();
|
||||
let current = cards
|
||||
.first()
|
||||
.and_then(|card| {
|
||||
ordered
|
||||
.iter()
|
||||
.position(|workspace| workspace.id == card.workspace)
|
||||
})
|
||||
.and_then(|card| ordered.iter().position(|workspace| workspace.id == card.workspace))
|
||||
.unwrap_or(0);
|
||||
let next = if delta > 0.0 {
|
||||
current.saturating_add(1)
|
||||
@@ -863,12 +786,7 @@ fn overview_layout(bounds: Rect, windows: &[(WindowId, Rect)]) -> HashMap<Window
|
||||
preview_layout(bounds, windows, OVERVIEW_MARGIN, OVERVIEW_GAP)
|
||||
}
|
||||
|
||||
fn preview_layout(
|
||||
bounds: Rect,
|
||||
windows: &[(WindowId, Rect)],
|
||||
margin: f64,
|
||||
gap: f64,
|
||||
) -> HashMap<WindowId, Rect> {
|
||||
fn preview_layout(bounds: Rect, windows: &[(WindowId, Rect)], margin: f64, gap: f64) -> HashMap<WindowId, Rect> {
|
||||
if windows.is_empty() {
|
||||
return HashMap::new();
|
||||
}
|
||||
@@ -881,10 +799,8 @@ fn preview_layout(
|
||||
let cell_size = |columns: usize| {
|
||||
let rows = windows.len().div_ceil(columns);
|
||||
(
|
||||
((content_width - gap * columns.saturating_sub(1) as f64) / columns as f64)
|
||||
.clamp(1.0, 480.0),
|
||||
((content_height - gap * rows.saturating_sub(1) as f64) / rows as f64)
|
||||
.clamp(1.0, 420.0),
|
||||
((content_width - gap * columns.saturating_sub(1) as f64) / columns as f64).clamp(1.0, 480.0),
|
||||
((content_height - gap * rows.saturating_sub(1) as f64) / rows as f64).clamp(1.0, 420.0),
|
||||
)
|
||||
};
|
||||
let score = |columns: usize| {
|
||||
@@ -926,10 +842,8 @@ fn preview_layout(
|
||||
let width = (source.width * scale).max(1.0);
|
||||
let height = (source.height * scale).max(1.0);
|
||||
let row_count = (windows.len() - row * columns).min(columns);
|
||||
let row_width =
|
||||
cell_width * row_count as f64 + gap * row_count.saturating_sub(1) as f64;
|
||||
let cell_x =
|
||||
bounds.x + (bounds.width - row_width) * 0.5 + column as f64 * (cell_width + gap);
|
||||
let row_width = cell_width * row_count as f64 + gap * row_count.saturating_sub(1) as f64;
|
||||
let cell_x = bounds.x + (bounds.width - row_width) * 0.5 + column as f64 * (cell_width + gap);
|
||||
let cell_y = origin_y + row as f64 * (cell_height + gap);
|
||||
let target = Rect::new(
|
||||
cell_x + (cell_width - width) / 2.0,
|
||||
@@ -947,11 +861,7 @@ fn rect_center(rect: Rect) -> (f64, f64) {
|
||||
(rect.x + rect.width / 2.0, rect.y + rect.height / 2.0)
|
||||
}
|
||||
|
||||
fn directional_distance(
|
||||
direction: Direction,
|
||||
current: (f64, f64),
|
||||
candidate: (f64, f64),
|
||||
) -> Option<f64> {
|
||||
fn directional_distance(direction: Direction, current: (f64, f64), candidate: (f64, f64)) -> Option<f64> {
|
||||
let delta_x = candidate.0 - current.0;
|
||||
let delta_y = candidate.1 - current.1;
|
||||
let (primary, secondary) = match direction {
|
||||
@@ -1001,10 +911,7 @@ mod tests {
|
||||
requests.request(family);
|
||||
}
|
||||
receiver.try_recv().unwrap();
|
||||
assert!(matches!(
|
||||
receiver.try_recv(),
|
||||
Err(mpsc::TryRecvError::Empty)
|
||||
));
|
||||
assert!(matches!(receiver.try_recv(), Err(mpsc::TryRecvError::Empty)));
|
||||
assert_eq!(*requests.latest.lock().unwrap(), "third");
|
||||
drop(requests);
|
||||
assert!(matches!(receiver.recv(), Err(mpsc::RecvError)));
|
||||
@@ -1024,9 +931,7 @@ mod tests {
|
||||
let strip = Rect::new(origin + 24.0, 64.0, 1552.0, 132.0);
|
||||
for count in [1, 3, 8] {
|
||||
let start = centered_row_start(strip, count, 160.0);
|
||||
let end = start
|
||||
+ count as f64 * 160.0
|
||||
+ count.saturating_sub(1) as f64 * WORKSPACE_CARD_GAP;
|
||||
let end = start + count as f64 * 160.0 + count.saturating_sub(1) as f64 * WORKSPACE_CARD_GAP;
|
||||
assert!((start - strip.x - (strip.x + strip.width - end)).abs() < 0.001);
|
||||
}
|
||||
}
|
||||
@@ -1043,11 +948,7 @@ mod tests {
|
||||
assert!(strip.height < bounds.height / 2.0);
|
||||
assert!(area.y > strip.y + strip.height);
|
||||
let windows = [(WindowId(1), Rect::new(0.0, 0.0, 800.0, 900.0))];
|
||||
assert!(
|
||||
overview_layout(area, &windows)
|
||||
.values()
|
||||
.all(|rect| rect.y >= area.y)
|
||||
);
|
||||
assert!(overview_layout(area, &windows).values().all(|rect| rect.y >= area.y));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1146,10 +1047,7 @@ mod tests {
|
||||
#[test]
|
||||
fn overview_grid_preserves_preview_aspect_ratio() {
|
||||
let source = Rect::new(0.0, 0.0, 1200.0, 800.0);
|
||||
let layout = overview_layout(
|
||||
Rect::new(0.0, 0.0, 1920.0, 1080.0),
|
||||
&[(WindowId(1), source)],
|
||||
);
|
||||
let layout = overview_layout(Rect::new(0.0, 0.0, 1920.0, 1080.0), &[(WindowId(1), source)]);
|
||||
let preview = layout[&WindowId(1)];
|
||||
|
||||
assert!((preview.width / preview.height - source.width / source.height).abs() < 0.001);
|
||||
@@ -1235,10 +1133,7 @@ mod tests {
|
||||
let mut overview = OverviewState::default();
|
||||
|
||||
overview.enter(
|
||||
HashMap::from([
|
||||
(left, (left_rect, left_rect)),
|
||||
(right, (right_rect, right_rect)),
|
||||
]),
|
||||
HashMap::from([(left, (left_rect, left_rect)), (right, (right_rect, right_rect))]),
|
||||
Some(left),
|
||||
false,
|
||||
);
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
use std::collections::{HashMap, HashSet, VecDeque};
|
||||
use std::sync::mpsc::SyncSender;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use ferese_ipc::Response;
|
||||
use serde::Deserialize;
|
||||
use serde_json::{Value, json};
|
||||
use smithay::reexports::calloop::timer::{TimeoutAction, Timer};
|
||||
use std::{
|
||||
collections::{HashMap, HashSet, VecDeque},
|
||||
sync::mpsc::SyncSender,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
@@ -104,31 +103,21 @@ impl PortalSession {
|
||||
|
||||
pub(crate) fn remove(&mut self, owner: u64) {
|
||||
self.waiters.remove(&owner);
|
||||
if self.phase == 2
|
||||
&& self
|
||||
.query
|
||||
.as_ref()
|
||||
.is_some_and(|query| query.owner == Some(owner))
|
||||
{
|
||||
if self.phase == 2 && self.query.as_ref().is_some_and(|query| query.owner == Some(owner)) {
|
||||
self.set_phase(1);
|
||||
}
|
||||
if self.inhibitors.remove(&owner).is_some() {
|
||||
self.inhibitor_revision = self.inhibitor_revision.wrapping_add(1);
|
||||
}
|
||||
let monitor_removed = self.monitors.remove(&owner);
|
||||
let acknowledged = self
|
||||
.query
|
||||
.as_mut()
|
||||
.is_some_and(|query| query.pending.remove(&owner));
|
||||
let acknowledged = self.query.as_mut().is_some_and(|query| query.pending.remove(&owner));
|
||||
if monitor_removed || acknowledged {
|
||||
self.changed();
|
||||
}
|
||||
}
|
||||
|
||||
fn idle_inhibited(&self) -> bool {
|
||||
self.inhibitors
|
||||
.values()
|
||||
.any(|inhibitor| inhibitor.flags & 8 != 0)
|
||||
self.inhibitors.values().any(|inhibitor| inhibitor.flags & 8 != 0)
|
||||
}
|
||||
|
||||
pub(crate) fn begin_query(&mut self) -> u32 {
|
||||
@@ -152,11 +141,7 @@ impl PortalSession {
|
||||
}
|
||||
|
||||
pub(crate) fn cancel_query(&mut self, token: u32) {
|
||||
if self
|
||||
.query
|
||||
.as_ref()
|
||||
.is_some_and(|query| query.token == token)
|
||||
{
|
||||
if self.query.as_ref().is_some_and(|query| query.token == token) {
|
||||
self.set_phase(1);
|
||||
}
|
||||
}
|
||||
@@ -169,42 +154,19 @@ impl PortalSession {
|
||||
.is_some_and(|query| query.pending.is_empty() || Instant::now() >= query.deadline)
|
||||
}
|
||||
|
||||
pub(crate) fn validate_end(
|
||||
&self,
|
||||
token: u32,
|
||||
inhibitor_revision: u32,
|
||||
force: bool,
|
||||
) -> Result<(), String> {
|
||||
if !self.query_ready()
|
||||
|| !self
|
||||
.query
|
||||
.as_ref()
|
||||
.is_some_and(|query| query.token == token)
|
||||
{
|
||||
return Err(
|
||||
"Session-ending confirmation expired or applications are still responding".into(),
|
||||
);
|
||||
pub(crate) fn validate_end(&self, token: u32, inhibitor_revision: u32, force: bool) -> Result<(), String> {
|
||||
if !self.query_ready() || !self.query.as_ref().is_some_and(|query| query.token == token) {
|
||||
return Err("Session-ending confirmation expired or applications are still responding".into());
|
||||
}
|
||||
if self.inhibitor_revision != inhibitor_revision
|
||||
|| (!force
|
||||
&& self
|
||||
.inhibitors
|
||||
.values()
|
||||
.any(|inhibitor| inhibitor.flags & 1 != 0))
|
||||
|| (!force && self.inhibitors.values().any(|inhibitor| inhibitor.flags & 1 != 0))
|
||||
{
|
||||
return Err(
|
||||
"Applications changed their inhibitors; review the confirmation again".into(),
|
||||
);
|
||||
return Err("Applications changed their inhibitors; review the confirmation again".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn commit_end(
|
||||
&mut self,
|
||||
token: u32,
|
||||
inhibitor_revision: u32,
|
||||
force: bool,
|
||||
) -> Result<(), String> {
|
||||
pub(crate) fn commit_end(&mut self, token: u32, inhibitor_revision: u32, force: bool) -> Result<(), String> {
|
||||
self.validate_end(token, inhibitor_revision, force)?;
|
||||
self.set_phase(3);
|
||||
Ok(())
|
||||
@@ -247,13 +209,7 @@ impl PortalSession {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn watch(
|
||||
&mut self,
|
||||
owner: u64,
|
||||
id: u64,
|
||||
since: u64,
|
||||
response: SyncSender<Response>,
|
||||
) {
|
||||
pub(crate) fn watch(&mut self, owner: u64, id: u64, since: u64, response: SyncSender<Response>) {
|
||||
if since != self.revision {
|
||||
let _ = response.try_send(Response::success(id, self.snapshot()));
|
||||
} else {
|
||||
@@ -262,12 +218,13 @@ impl PortalSession {
|
||||
}
|
||||
|
||||
pub(crate) fn snapshot(&self) -> Value {
|
||||
let inhibitors = self.inhibitors.values().map(|inhibitor| json!({"flags": inhibitor.flags, "app": inhibitor.app, "reason": inhibitor.reason})).collect::<Vec<_>>();
|
||||
let inhibitors = self
|
||||
.inhibitors
|
||||
.values()
|
||||
.map(|inhibitor| json!({"flags": inhibitor.flags, "app": inhibitor.app, "reason": inhibitor.reason}))
|
||||
.collect::<Vec<_>>();
|
||||
let remaining = self.query.as_ref().map_or(0, |query| {
|
||||
query
|
||||
.deadline
|
||||
.saturating_duration_since(Instant::now())
|
||||
.as_millis() as u64
|
||||
query.deadline.saturating_duration_since(Instant::now()).as_millis() as u64
|
||||
});
|
||||
json!({"screensaver-active": self.locked, "session-state": self.phase, "revision": self.revision,
|
||||
"inhibitors": inhibitors, "inhibitor-revision": self.inhibitor_revision,
|
||||
@@ -278,24 +235,22 @@ impl PortalSession {
|
||||
|
||||
impl crate::Ferese {
|
||||
pub(crate) fn refresh_idle_inhibition(&mut self) {
|
||||
self.idle_notifier_state.set_is_inhibited(
|
||||
!self.idle_inhibitors.is_empty() || self.portal_session.idle_inhibited(),
|
||||
);
|
||||
self.idle_notifier_state
|
||||
.set_is_inhibited(!self.idle_inhibitors.is_empty() || self.portal_session.idle_inhibited());
|
||||
}
|
||||
|
||||
pub(crate) fn end_portal_session(&mut self) {
|
||||
let token = self.portal_session.query.as_ref().map(|query| query.token);
|
||||
let result = self.loop_handle.insert_source(
|
||||
Timer::from_duration(Duration::from_millis(200)),
|
||||
move |_, _, state| {
|
||||
if state.portal_session.phase == 3
|
||||
&& state.portal_session.query.as_ref().map(|query| query.token) == token
|
||||
{
|
||||
state.loop_signal.stop();
|
||||
}
|
||||
TimeoutAction::Drop
|
||||
},
|
||||
);
|
||||
let result =
|
||||
self.loop_handle
|
||||
.insert_source(Timer::from_duration(Duration::from_millis(200)), move |_, _, state| {
|
||||
if state.portal_session.phase == 3
|
||||
&& state.portal_session.query.as_ref().map(|query| query.token) == token
|
||||
{
|
||||
state.loop_signal.stop();
|
||||
}
|
||||
TimeoutAction::Drop
|
||||
});
|
||||
if let Err(error) = result {
|
||||
tracing::error!(%error, "failed to schedule session ending");
|
||||
self.loop_signal.stop();
|
||||
@@ -354,20 +309,10 @@ mod tests {
|
||||
let approved = session.inhibitor_revision;
|
||||
session.register(1, inhibition(1)).unwrap();
|
||||
assert!(session.commit_end(token, approved, true).is_err());
|
||||
assert!(
|
||||
session
|
||||
.commit_end(token, session.inhibitor_revision, false)
|
||||
.is_err()
|
||||
);
|
||||
session
|
||||
.commit_end(token, session.inhibitor_revision, true)
|
||||
.unwrap();
|
||||
assert!(session.commit_end(token, session.inhibitor_revision, false).is_err());
|
||||
session.commit_end(token, session.inhibitor_revision, true).unwrap();
|
||||
assert_eq!(session.snapshot()["session-state"], 3);
|
||||
assert!(
|
||||
session
|
||||
.commit_end(token, session.inhibitor_revision, true)
|
||||
.is_err()
|
||||
);
|
||||
assert!(session.commit_end(token, session.inhibitor_revision, true).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -410,10 +355,7 @@ mod tests {
|
||||
let response = receiver.try_recv().unwrap();
|
||||
assert_eq!(response.result.unwrap()["screensaver-active"], true);
|
||||
session.set_locked(false);
|
||||
assert_eq!(
|
||||
session.snapshot()["transitions"].as_array().unwrap().len(),
|
||||
2
|
||||
);
|
||||
assert_eq!(session.snapshot()["transitions"].as_array().unwrap().len(), 2);
|
||||
let (sender, receiver) = std::sync::mpsc::sync_channel(1);
|
||||
session.watch(1, 11, session.revision, sender);
|
||||
session.remove(1);
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
use std::{
|
||||
collections::{HashMap, VecDeque},
|
||||
time::Instant,
|
||||
};
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::time::Instant;
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde_json::{Value, json};
|
||||
@@ -28,9 +26,7 @@ struct Registration {
|
||||
|
||||
impl Registration {
|
||||
fn timestamp(&self) -> u64 {
|
||||
self.latest
|
||||
.0
|
||||
.saturating_add(self.latest.1.elapsed().as_millis() as u64)
|
||||
self.latest.0.saturating_add(self.latest.1.elapsed().as_millis() as u64)
|
||||
}
|
||||
|
||||
fn release_all(&mut self) {
|
||||
@@ -57,11 +53,7 @@ impl PortalShortcuts {
|
||||
return Err("Shortcut limit reached".into());
|
||||
}
|
||||
let keymap = crate::config::physical_keymap(input).map_err(|error| error.to_string())?;
|
||||
if self
|
||||
.0
|
||||
.get(&owner)
|
||||
.is_some_and(|registration| registration.closed)
|
||||
{
|
||||
if self.0.get(&owner).is_some_and(|registration| registration.closed) {
|
||||
return Err("Shortcut session has ended".into());
|
||||
}
|
||||
let mut parsed: Vec<(Shortcut, Binding)> = Vec::new();
|
||||
@@ -70,17 +62,11 @@ impl PortalShortcuts {
|
||||
.trigger
|
||||
.split('+')
|
||||
.any(|part| part.trim().eq_ignore_ascii_case("num"));
|
||||
if shortcut.id.is_empty()
|
||||
|| shortcut.id.len() > 256
|
||||
|| shortcut.id.chars().any(char::is_control)
|
||||
{
|
||||
if shortcut.id.is_empty() || shortcut.id.len() > 256 || shortcut.id.chars().any(char::is_control) {
|
||||
return Err("Invalid shortcut identifier".into());
|
||||
}
|
||||
let binding =
|
||||
Binding::portal_trigger(&shortcut.trigger).map_err(|error| error.to_string())?;
|
||||
if configured
|
||||
.iter()
|
||||
.any(|other| binding.conflicts(other, &keymap))
|
||||
let binding = Binding::portal_trigger(&shortcut.trigger).map_err(|error| error.to_string())?;
|
||||
if configured.iter().any(|other| binding.conflicts(other, &keymap))
|
||||
|| parsed
|
||||
.iter()
|
||||
.any(|(other, key)| other.id == shortcut.id || binding.conflicts(key, &keymap))
|
||||
@@ -120,9 +106,9 @@ impl PortalShortcuts {
|
||||
return;
|
||||
};
|
||||
for registration in self.0.values_mut() {
|
||||
registration.shortcuts.retain(|(_, binding)| {
|
||||
!configured.iter().any(|key| binding.conflicts(key, &keymap))
|
||||
});
|
||||
registration
|
||||
.shortcuts
|
||||
.retain(|(_, binding)| !configured.iter().any(|key| binding.conflicts(key, &keymap)));
|
||||
let active_ids = registration
|
||||
.shortcuts
|
||||
.iter()
|
||||
@@ -158,13 +144,7 @@ impl PortalShortcuts {
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn press(
|
||||
&mut self,
|
||||
key: Keycode,
|
||||
symbols: &[u32],
|
||||
modifiers: &ModifiersState,
|
||||
timestamp: u64,
|
||||
) -> bool {
|
||||
pub(crate) fn press(&mut self, key: Keycode, symbols: &[u32], modifiers: &ModifiersState, timestamp: u64) -> bool {
|
||||
for registration in self.0.values_mut() {
|
||||
let Some((shortcut, _)) = registration.shortcuts.iter().find(|(shortcut, binding)| {
|
||||
(!shortcut.num_required || modifiers.num_lock)
|
||||
@@ -212,9 +192,10 @@ impl PortalShortcuts {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use smithay::input::keyboard::keysyms;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn shortcut(id: &str, trigger: &str) -> Shortcut {
|
||||
Shortcut {
|
||||
id: id.into(),
|
||||
@@ -244,10 +225,7 @@ mod tests {
|
||||
.register(1, vec![shortcut("one", "Super+q")], &configured, &input)
|
||||
.is_err()
|
||||
);
|
||||
assert_eq!(
|
||||
shortcuts.poll(1, false)["shortcuts"][0]["trigger"],
|
||||
"Ctrl+Alt+a"
|
||||
);
|
||||
assert_eq!(shortcuts.poll(1, false)["shortcuts"][0]["trigger"], "Ctrl+Alt+a");
|
||||
shortcuts.remove(1);
|
||||
assert_eq!(shortcuts.poll(1, false)["closed"], true);
|
||||
shortcuts
|
||||
@@ -296,12 +274,7 @@ mod tests {
|
||||
.register(1, vec![shortcut("one", "Ctrl+Alt+a")], &[], &input)
|
||||
.unwrap();
|
||||
shortcuts.reconcile(&[Binding::portal_trigger("Ctrl+Alt+a").unwrap()], &input);
|
||||
assert!(
|
||||
shortcuts.poll(1, false)["shortcuts"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.is_empty()
|
||||
);
|
||||
assert!(shortcuts.poll(1, false)["shortcuts"].as_array().unwrap().is_empty());
|
||||
assert!(Binding::portal_trigger("Ctrl+Alt+Escape").is_err());
|
||||
assert!(Binding::portal_trigger("Ctrl+Alt+F3").is_err());
|
||||
}
|
||||
@@ -332,10 +305,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn physical_conflicts_include_secondary_layouts_and_num_modifier_is_honored() {
|
||||
let config = crate::config::Config::parse_source(
|
||||
"binding keys=\"Ctrl+AD01\" match=\"physical\" action=\"none\"\n",
|
||||
)
|
||||
.unwrap();
|
||||
let config =
|
||||
crate::config::Config::parse_source("binding keys=\"Ctrl+AD01\" match=\"physical\" action=\"none\"\n")
|
||||
.unwrap();
|
||||
let mut input = config.input_settings().unwrap();
|
||||
input.xkb_layout = "us,fr".into();
|
||||
let configured = config.bindings(&input).unwrap();
|
||||
|
||||
+110
-54
@@ -2,21 +2,63 @@
|
||||
//! logical pixels. Content, clips and decorations share those exact edges.
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use smithay::{
|
||||
backend::renderer::{
|
||||
element::{Element, Id, Kind, RenderElement},
|
||||
gles::{
|
||||
GlesError, GlesFrame, GlesRenderer, GlesTexProgram, GlesTexture, Uniform,
|
||||
element::PixelShaderElement,
|
||||
},
|
||||
utils::{CommitCounter, DamageSet},
|
||||
},
|
||||
utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Transform},
|
||||
use smithay::backend::renderer::element::{
|
||||
Element, Id, Kind, RenderElement, RenderElementPresentationState, RenderElementStates,
|
||||
};
|
||||
use smithay::backend::renderer::gles::element::PixelShaderElement;
|
||||
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer, GlesTexProgram, GlesTexture, Uniform};
|
||||
use smithay::backend::renderer::utils::{CommitCounter, DamageSet};
|
||||
use smithay::desktop::utils::OutputPresentationFeedback;
|
||||
use smithay::output::Output;
|
||||
use smithay::reexports::wayland_protocols::wp::presentation_time::server::wp_presentation_feedback;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Transform};
|
||||
use smithay::wayland::compositor::SurfaceData;
|
||||
|
||||
pub(crate) const HANDOFF: Duration = Duration::from_millis(80);
|
||||
pub(crate) const SNAPSHOT_BUDGET: usize = 64 * 1024 * 1024;
|
||||
|
||||
pub(crate) fn take_output_feedback(
|
||||
state: &crate::Ferese,
|
||||
output: &Output,
|
||||
rendered: &RenderElementStates,
|
||||
flags: wp_presentation_feedback::Kind,
|
||||
) -> OutputPresentationFeedback {
|
||||
use smithay::desktop::layer_map_for_output;
|
||||
use smithay::desktop::utils::{OutputPresentationFeedback, surface_presentation_feedback_flags_from_states};
|
||||
|
||||
let mut feedback = OutputPresentationFeedback::new(output);
|
||||
let visible_output =
|
||||
|surface: &WlSurface, _: &SurfaceData| rendered_feedback_output(output, surface.into(), rendered);
|
||||
let surface_flags = |surface: &WlSurface, _: &SurfaceData| {
|
||||
flags | surface_presentation_feedback_flags_from_states(surface, rendered)
|
||||
};
|
||||
|
||||
for window in state.space.elements().filter(|window| {
|
||||
state
|
||||
.window_ids
|
||||
.get(*window)
|
||||
.is_some_and(|id| state.window_belongs_to_output(*id, output))
|
||||
}) {
|
||||
window.take_presentation_feedback(&mut feedback, visible_output, surface_flags);
|
||||
}
|
||||
|
||||
for layer in layer_map_for_output(output).layers() {
|
||||
layer.take_presentation_feedback(&mut feedback, visible_output, surface_flags);
|
||||
}
|
||||
|
||||
feedback
|
||||
}
|
||||
|
||||
fn rendered_feedback_output(output: &Output, id: Id, rendered: &RenderElementStates) -> Option<Output> {
|
||||
rendered
|
||||
.element_render_state(id)
|
||||
.is_some_and(|state| {
|
||||
state.visible_area > 0 && state.presentation_state != RenderElementPresentationState::Skipped
|
||||
})
|
||||
.then(|| output.clone())
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct CallbackClock {
|
||||
last_sent: Option<Instant>,
|
||||
@@ -24,8 +66,7 @@ pub(crate) struct CallbackClock {
|
||||
|
||||
impl CallbackClock {
|
||||
pub(crate) fn deadline(&self, now: Instant, interval: Duration) -> Instant {
|
||||
self.last_sent
|
||||
.map_or(now, |last| (last + interval).max(now))
|
||||
self.last_sent.map_or(now, |last| (last + interval).max(now))
|
||||
}
|
||||
|
||||
pub(crate) fn sent(&mut self, now: Instant) {
|
||||
@@ -48,6 +89,7 @@ pub(crate) fn physical_rect(
|
||||
let top = ((rect.y - f64::from(origin.y)) * scale).round() as i32;
|
||||
let right = ((rect.x + rect.width - f64::from(origin.x)) * scale).round() as i32;
|
||||
let bottom = ((rect.y + rect.height - f64::from(origin.y)) * scale).round() as i32;
|
||||
|
||||
Rectangle::new(
|
||||
(left, top).into(),
|
||||
((right - left).max(1), (bottom - top).max(1)).into(),
|
||||
@@ -122,11 +164,7 @@ impl Element for PhysicalShaderElement {
|
||||
self.geometry
|
||||
}
|
||||
|
||||
fn damage_since(
|
||||
&self,
|
||||
_: Scale<f64>,
|
||||
commit: Option<CommitCounter>,
|
||||
) -> DamageSet<i32, Physical> {
|
||||
fn damage_since(&self, _: Scale<f64>, commit: Option<CommitCounter>) -> DamageSet<i32, Physical> {
|
||||
if commit == Some(self.current_commit()) {
|
||||
DamageSet::default()
|
||||
} else {
|
||||
@@ -187,11 +225,7 @@ impl Element for NativeTextureElement {
|
||||
Kind::Unspecified
|
||||
}
|
||||
|
||||
fn damage_since(
|
||||
&self,
|
||||
_: Scale<f64>,
|
||||
commit: Option<CommitCounter>,
|
||||
) -> DamageSet<i32, Physical> {
|
||||
fn damage_since(&self, _: Scale<f64>, commit: Option<CommitCounter>) -> DamageSet<i32, Physical> {
|
||||
if commit == Some(self.commit) {
|
||||
DamageSet::default()
|
||||
} else {
|
||||
@@ -227,26 +261,59 @@ impl RenderElement<GlesRenderer> for NativeTextureElement {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn presentation_feedback_excludes_missing_and_occluded_surfaces() {
|
||||
use smithay::backend::renderer::element::RenderElementState;
|
||||
use smithay::output::{PhysicalProperties, Subpixel};
|
||||
|
||||
let output = Output::new(
|
||||
"owner".into(),
|
||||
PhysicalProperties {
|
||||
size: (0, 0).into(),
|
||||
subpixel: Subpixel::Unknown,
|
||||
make: "test".into(),
|
||||
model: "test".into(),
|
||||
},
|
||||
);
|
||||
let id = Id::new();
|
||||
let mut states = RenderElementStates::default();
|
||||
assert!(rendered_feedback_output(&output, id.clone(), &states).is_none());
|
||||
for state in [
|
||||
RenderElementState {
|
||||
visible_area: 0,
|
||||
presentation_state: RenderElementPresentationState::Skipped,
|
||||
},
|
||||
RenderElementState {
|
||||
visible_area: 0,
|
||||
presentation_state: RenderElementPresentationState::Rendering { reason: None },
|
||||
},
|
||||
] {
|
||||
states.states.insert(id.clone(), state);
|
||||
assert!(rendered_feedback_output(&output, id.clone(), &states).is_none());
|
||||
}
|
||||
for presentation_state in [
|
||||
RenderElementPresentationState::Rendering { reason: None },
|
||||
RenderElementPresentationState::ZeroCopy,
|
||||
] {
|
||||
states.states.insert(
|
||||
id.clone(),
|
||||
RenderElementState {
|
||||
visible_area: 32,
|
||||
presentation_state,
|
||||
},
|
||||
);
|
||||
assert_eq!(
|
||||
rendered_feedback_output(&output, id.clone(), &states),
|
||||
Some(output.clone())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reversing_a_grow_replaces_the_smaller_snapshot() {
|
||||
assert!(!snapshot_covers_source(
|
||||
(500, 600).into(),
|
||||
(1000, 600).into(),
|
||||
1.0,
|
||||
1.0
|
||||
));
|
||||
assert!(snapshot_covers_source(
|
||||
(1000, 600).into(),
|
||||
(500, 600).into(),
|
||||
1.0,
|
||||
1.0
|
||||
));
|
||||
assert!(!snapshot_covers_source(
|
||||
(1000, 600).into(),
|
||||
(500, 600).into(),
|
||||
1.0,
|
||||
1.8
|
||||
));
|
||||
assert!(!snapshot_covers_source((500, 600).into(), (1000, 600).into(), 1.0, 1.0));
|
||||
assert!(snapshot_covers_source((1000, 600).into(), (500, 600).into(), 1.0, 1.0));
|
||||
assert!(!snapshot_covers_source((1000, 600).into(), (500, 600).into(), 1.0, 1.8));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -347,25 +414,14 @@ mod tests {
|
||||
fn fractional_motion_does_not_quantize_to_logical_pixels() {
|
||||
let rect = ferese_layout::Rect::new(0.3, 0.3, 100.2, 80.2);
|
||||
assert_eq!(physical_rect(rect, (0, 0).into(), 1.8).loc, (1, 1).into());
|
||||
assert_eq!(
|
||||
physical_rect(rect, (0, 0).into(), 1.8).size,
|
||||
(180, 144).into()
|
||||
);
|
||||
assert_eq!(physical_rect(rect, (0, 0).into(), 1.8).size, (180, 144).into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adjacent_frames_share_edges_at_all_output_scales() {
|
||||
for scale in [1.0, 1.25, 1.5, 1.8, 2.0] {
|
||||
let left = physical_rect(
|
||||
ferese_layout::Rect::new(13.3, 0.0, 500.4, 800.0),
|
||||
(0, 0).into(),
|
||||
scale,
|
||||
);
|
||||
let right = physical_rect(
|
||||
ferese_layout::Rect::new(513.7, 0.0, 500.4, 800.0),
|
||||
(0, 0).into(),
|
||||
scale,
|
||||
);
|
||||
let left = physical_rect(ferese_layout::Rect::new(13.3, 0.0, 500.4, 800.0), (0, 0).into(), scale);
|
||||
let right = physical_rect(ferese_layout::Rect::new(513.7, 0.0, 500.4, 800.0), (0, 0).into(), scale);
|
||||
assert_eq!(left.loc.x + left.size.w, right.loc.x);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
use std::{
|
||||
io,
|
||||
os::{
|
||||
fd::AsRawFd,
|
||||
unix::{net::UnixStream, process::CommandExt},
|
||||
},
|
||||
process::Command,
|
||||
sync::Arc,
|
||||
};
|
||||
use std::io;
|
||||
use std::os::fd::AsRawFd;
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::os::unix::process::CommandExt;
|
||||
use std::process::Command;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{Ferese, state::ClientState};
|
||||
use crate::Ferese;
|
||||
use crate::state::ClientState;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub(crate) struct ClientCapabilities(u8);
|
||||
@@ -36,8 +34,8 @@ pub(crate) fn prepare_command(
|
||||
command: &mut Command,
|
||||
capabilities: ClientCapabilities,
|
||||
) -> io::Result<Vec<UnixStream>> {
|
||||
let split_shell_control = capabilities.contains(ClientCapabilities::EFFECTS)
|
||||
&& capabilities.contains(ClientCapabilities::SHELL_CONTROL);
|
||||
let split_shell_control =
|
||||
capabilities.contains(ClientCapabilities::EFFECTS) && capabilities.contains(ClientCapabilities::SHELL_CONTROL);
|
||||
let primary_capabilities = if split_shell_control {
|
||||
ClientCapabilities::EFFECTS
|
||||
} else {
|
||||
@@ -56,10 +54,7 @@ pub(crate) fn prepare_command(
|
||||
if split_shell_control {
|
||||
let control = insert_private_client(state, ClientCapabilities::SHELL_CONTROL)?;
|
||||
|
||||
command.env(
|
||||
"FERESE_SHELL_CONTROL_SOCKET",
|
||||
control.as_raw_fd().to_string(),
|
||||
);
|
||||
command.env("FERESE_SHELL_CONTROL_SOCKET", control.as_raw_fd().to_string());
|
||||
clients.push(control);
|
||||
}
|
||||
|
||||
@@ -81,10 +76,7 @@ pub(crate) fn prepare_command(
|
||||
Ok(clients)
|
||||
}
|
||||
|
||||
fn insert_private_client(
|
||||
state: &mut Ferese,
|
||||
capabilities: ClientCapabilities,
|
||||
) -> io::Result<UnixStream> {
|
||||
fn insert_private_client(state: &mut Ferese, capabilities: ClientCapabilities) -> io::Result<UnixStream> {
|
||||
let (server, client) = UnixStream::pair()?;
|
||||
|
||||
state.display_handle.insert_client(
|
||||
|
||||
+18
-34
@@ -1,11 +1,10 @@
|
||||
//! Filesystem events wake the loop; only pending edits need a debounce timer.
|
||||
use std::io::Read;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::mpsc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use notify::{RecommendedWatcher, RecursiveMode, Watcher};
|
||||
use std::{
|
||||
io::Read,
|
||||
path::PathBuf,
|
||||
sync::mpsc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
fn read_source(path: &std::path::Path) -> Result<String, String> {
|
||||
let file = std::fs::File::open(path).map_err(|e| e.to_string())?;
|
||||
@@ -40,21 +39,17 @@ impl ConfigMonitor {
|
||||
let (sender, events) = mpsc::sync_channel(1);
|
||||
let target = path.clone();
|
||||
let event_wakeup = wakeup.clone();
|
||||
let mut watcher =
|
||||
notify::recommended_watcher(move |event: notify::Result<notify::Event>| {
|
||||
if let Ok(event) = event
|
||||
&& !event.kind.is_access()
|
||||
&& event
|
||||
.paths
|
||||
.iter()
|
||||
.any(|p| p == &target || target.starts_with(p))
|
||||
{
|
||||
let _ = sender.try_send(());
|
||||
if let Some(wakeup) = &event_wakeup {
|
||||
wakeup.wakeup();
|
||||
}
|
||||
let mut watcher = notify::recommended_watcher(move |event: notify::Result<notify::Event>| {
|
||||
if let Ok(event) = event
|
||||
&& !event.kind.is_access()
|
||||
&& event.paths.iter().any(|p| p == &target || target.starts_with(p))
|
||||
{
|
||||
let _ = sender.try_send(());
|
||||
if let Some(wakeup) = &event_wakeup {
|
||||
wakeup.wakeup();
|
||||
}
|
||||
})?;
|
||||
}
|
||||
})?;
|
||||
// Watch the directory, not the inode: atomic editor saves replace it.
|
||||
let mut directory = path.parent().unwrap_or(std::path::Path::new("."));
|
||||
while !directory.is_dir() {
|
||||
@@ -224,10 +219,7 @@ mod tests {
|
||||
assert_eq!(result.unwrap(), "animations {\n speed 0.75\n}\n");
|
||||
break;
|
||||
}
|
||||
assert!(
|
||||
Instant::now() < deadline,
|
||||
"atomic save notification never settled"
|
||||
);
|
||||
assert!(Instant::now() < deadline, "atomic save notification never settled");
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
}
|
||||
@@ -242,17 +234,9 @@ mod tests {
|
||||
let mut monitor = ConfigMonitor::new(path).unwrap();
|
||||
let now = Instant::now();
|
||||
monitor.dirty = Some(now);
|
||||
assert_eq!(
|
||||
monitor.next_deadline(),
|
||||
Some(now + Duration::from_millis(120))
|
||||
);
|
||||
assert_eq!(monitor.next_deadline(), Some(now + Duration::from_millis(120)));
|
||||
assert!(monitor.poll(now + Duration::from_millis(119)).is_none());
|
||||
assert!(
|
||||
monitor
|
||||
.poll(now + Duration::from_millis(121))
|
||||
.unwrap()
|
||||
.is_ok()
|
||||
);
|
||||
assert!(monitor.poll(now + Duration::from_millis(121)).unwrap().is_ok());
|
||||
assert_eq!(monitor.next_deadline(), None);
|
||||
}
|
||||
}
|
||||
@@ -72,8 +72,7 @@ mod tests {
|
||||
#[test]
|
||||
fn shrinking_commit_retains_the_previous_full_frame_geometry() {
|
||||
let source = Rectangle::new((4, 8).into(), (1200, 800).into());
|
||||
let transaction =
|
||||
ResizeTransaction::new(1.into(), Duration::ZERO).with_source_geometry(source);
|
||||
let transaction = ResizeTransaction::new(1.into(), Duration::ZERO).with_source_geometry(source);
|
||||
assert_eq!(transaction.source_geometry(), Some(source));
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,21 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use smithay::output::Output;
|
||||
use smithay::reexports::calloop::RegistrationToken;
|
||||
use smithay::reexports::calloop::timer::{TimeoutAction, Timer};
|
||||
use smithay::reexports::wayland_protocols::ext::session_lock::v1::server::ext_session_lock_manager_v1::ExtSessionLockManagerV1;
|
||||
use smithay::reexports::wayland_protocols::ext::session_lock::v1::server::ext_session_lock_surface_v1::ExtSessionLockSurfaceV1;
|
||||
use smithay::reexports::wayland_protocols::ext::session_lock::v1::server::ext_session_lock_v1;
|
||||
use smithay::reexports::wayland_protocols::ext::session_lock::v1::server::ext_session_lock_v1::ExtSessionLockV1;
|
||||
use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
|
||||
use smithay::reexports::wayland_server::{Client, DataInit, Dispatch, DisplayHandle, Resource};
|
||||
use smithay::utils::{Logical, Point, SERIAL_COUNTER};
|
||||
use smithay::wayland::session_lock::{
|
||||
LockSurface, SessionLockHandler, SessionLockManagerState, SessionLockState, SessionLocker,
|
||||
};
|
||||
|
||||
use crate::Ferese;
|
||||
use smithay::reexports::calloop::{
|
||||
RegistrationToken,
|
||||
timer::{TimeoutAction, Timer},
|
||||
};
|
||||
use smithay::{
|
||||
output::Output,
|
||||
reexports::{
|
||||
wayland_protocols::ext::session_lock::v1::server::{
|
||||
ext_session_lock_manager_v1::ExtSessionLockManagerV1,
|
||||
ext_session_lock_surface_v1::ExtSessionLockSurfaceV1,
|
||||
ext_session_lock_v1::{self, ExtSessionLockV1},
|
||||
},
|
||||
wayland_server::{
|
||||
Client, DataInit, Dispatch, DisplayHandle, Resource, protocol::wl_output::WlOutput,
|
||||
},
|
||||
},
|
||||
utils::{Logical, Point, SERIAL_COUNTER},
|
||||
wayland::session_lock::{
|
||||
LockSurface, SessionLockHandler, SessionLockManagerState, SessionLockState, SessionLocker,
|
||||
},
|
||||
};
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug, serde::Deserialize)]
|
||||
#[serde(default)]
|
||||
@@ -54,10 +46,7 @@ impl IdleSettings {
|
||||
|
||||
fn next_poll(self, elapsed: Duration, animate: bool) -> Duration {
|
||||
let dim_at = Duration::from_secs(self.dim_after_seconds);
|
||||
if animate
|
||||
&& self.dim_after_seconds != 0
|
||||
&& elapsed >= dim_at
|
||||
&& elapsed < dim_at + Duration::from_millis(500)
|
||||
if animate && self.dim_after_seconds != 0 && elapsed >= dim_at && elapsed < dim_at + Duration::from_millis(500)
|
||||
{
|
||||
return Duration::from_millis(16);
|
||||
}
|
||||
@@ -72,8 +61,7 @@ impl IdleSettings {
|
||||
}
|
||||
|
||||
fn appearance(self, elapsed: Duration) -> (f32, bool) {
|
||||
let sleeping = self.sleep_after_seconds != 0
|
||||
&& elapsed >= Duration::from_secs(self.sleep_after_seconds);
|
||||
let sleeping = self.sleep_after_seconds != 0 && elapsed >= Duration::from_secs(self.sleep_after_seconds);
|
||||
let dim = if self.dim_after_seconds == 0 {
|
||||
0.0
|
||||
} else {
|
||||
@@ -99,17 +87,13 @@ pub(crate) struct Lock {
|
||||
idle_since: Option<Instant>,
|
||||
pub(crate) idle_opacity: f32,
|
||||
pub(crate) sleeping: bool,
|
||||
pub(crate) idle_overlays:
|
||||
HashMap<Output, smithay::backend::renderer::element::solid::SolidColorBuffer>,
|
||||
pub(crate) idle_overlays: HashMap<Output, smithay::backend::renderer::element::solid::SolidColorBuffer>,
|
||||
idle_timer: Option<RegistrationToken>,
|
||||
}
|
||||
|
||||
impl Lock {
|
||||
fn ready_for_idle<'a>(&self, outputs: impl Iterator<Item = &'a Output>) -> bool {
|
||||
self.active
|
||||
&& outputs
|
||||
.into_iter()
|
||||
.all(|output| self.presented.contains(output))
|
||||
self.active && outputs.into_iter().all(|output| self.presented.contains(output))
|
||||
}
|
||||
|
||||
pub(crate) fn output_added(&mut self, output: &Output) {
|
||||
@@ -146,14 +130,11 @@ impl Ferese {
|
||||
return;
|
||||
}
|
||||
let since = self.session_lock.idle_since.get_or_insert(now);
|
||||
let (mut opacity, sleeping) = self
|
||||
.lock_idle
|
||||
.appearance(now.saturating_duration_since(*since));
|
||||
let (mut opacity, sleeping) = self.lock_idle.appearance(now.saturating_duration_since(*since));
|
||||
if !self.animations_enabled() && opacity > 0.0 && !sleeping {
|
||||
opacity = 0.65;
|
||||
}
|
||||
let changed =
|
||||
opacity != self.session_lock.idle_opacity || sleeping != self.session_lock.sleeping;
|
||||
let changed = opacity != self.session_lock.idle_opacity || sleeping != self.session_lock.sleeping;
|
||||
let was_sleeping = self.session_lock.sleeping;
|
||||
self.session_lock.idle_opacity = opacity;
|
||||
self.session_lock.sleeping = sleeping;
|
||||
@@ -190,12 +171,7 @@ impl Ferese {
|
||||
let surface = self
|
||||
.focused_output()
|
||||
.and_then(|output| self.session_lock.surfaces.get(output))
|
||||
.or_else(|| {
|
||||
self.session_lock
|
||||
.surfaces
|
||||
.values()
|
||||
.find(|surface| surface.alive())
|
||||
})
|
||||
.or_else(|| self.session_lock.surfaces.values().find(|surface| surface.alive()))
|
||||
.filter(|surface| surface.alive())
|
||||
.map(|surface| surface.wl_surface().clone());
|
||||
self.seat
|
||||
@@ -234,12 +210,7 @@ impl Ferese {
|
||||
}
|
||||
|
||||
pub(crate) fn lock_frame_callbacks(&self, output: &Output) {
|
||||
if let Some(surface) = self
|
||||
.session_lock
|
||||
.surfaces
|
||||
.get(output)
|
||||
.filter(|surface| surface.alive())
|
||||
{
|
||||
if let Some(surface) = self.session_lock.surfaces.get(output).filter(|surface| surface.alive()) {
|
||||
smithay::desktop::utils::send_frames_surface_tree(
|
||||
surface.wl_surface(),
|
||||
output,
|
||||
@@ -263,9 +234,9 @@ impl SessionLockHandler for Ferese {
|
||||
self.cancel_logout_confirmation();
|
||||
self.session_lock.active = true;
|
||||
self.session_lock.idle_since = Some(Instant::now());
|
||||
match self.loop_handle.insert_source(
|
||||
Timer::from_duration(Duration::from_secs(1)),
|
||||
|_, _, state| {
|
||||
match self
|
||||
.loop_handle
|
||||
.insert_source(Timer::from_duration(Duration::from_secs(1)), |_, _, state| {
|
||||
if !state.session_lock.active {
|
||||
return TimeoutAction::Drop;
|
||||
}
|
||||
@@ -274,13 +245,8 @@ impl SessionLockHandler for Ferese {
|
||||
.session_lock
|
||||
.idle_since
|
||||
.map_or(Duration::ZERO, |since| since.elapsed());
|
||||
TimeoutAction::ToDuration(
|
||||
state
|
||||
.lock_idle
|
||||
.next_poll(elapsed, state.animations_enabled()),
|
||||
)
|
||||
},
|
||||
) {
|
||||
TimeoutAction::ToDuration(state.lock_idle.next_poll(elapsed, state.animations_enabled()))
|
||||
}) {
|
||||
Ok(token) => self.session_lock.idle_timer = Some(token),
|
||||
Err(error) => tracing::warn!(%error, "could not schedule lock screen inactivity"),
|
||||
}
|
||||
@@ -295,10 +261,7 @@ impl SessionLockHandler for Ferese {
|
||||
// already encoding has no pending capture left, so it is terminated
|
||||
// here and its staged file is discarded when the worker reports back.
|
||||
for request in self.screenshot.terminate_all() {
|
||||
tracing::debug!(
|
||||
request,
|
||||
"cancelled an in-flight screenshot for the session lock"
|
||||
);
|
||||
tracing::debug!(request, "cancelled an in-flight screenshot for the session lock");
|
||||
}
|
||||
self.set_overview_active(false);
|
||||
let serial = SERIAL_COUNTER.next_serial();
|
||||
@@ -345,9 +308,8 @@ impl SessionLockHandler for Ferese {
|
||||
if let Some(output) = Output::from_resource(&output)
|
||||
&& let Some(geometry) = self.space.output_geometry(&output)
|
||||
{
|
||||
surface.with_pending_state(|state| {
|
||||
state.size = Some((geometry.size.w as u32, geometry.size.h as u32).into())
|
||||
});
|
||||
surface
|
||||
.with_pending_state(|state| state.size = Some((geometry.size.w as u32, geometry.size.h as u32).into()));
|
||||
self.session_lock.surfaces.insert(output, surface);
|
||||
self.focus_lock_surface();
|
||||
}
|
||||
@@ -377,10 +339,7 @@ impl Dispatch<ExtSessionLockV1, SessionLockState> for Ferese {
|
||||
return;
|
||||
}
|
||||
if matches!(request, ext_session_lock_v1::Request::GetLockSurface { .. }) && !owner {
|
||||
lock.post_error(
|
||||
ext_session_lock_v1::Error::InvalidUnlock,
|
||||
"not the active lock owner",
|
||||
);
|
||||
lock.post_error(ext_session_lock_v1::Error::InvalidUnlock, "not the active lock owner");
|
||||
return;
|
||||
}
|
||||
<SessionLockManagerState as Dispatch<ExtSessionLockV1, SessionLockState, Ferese>>::request(
|
||||
@@ -413,10 +372,7 @@ mod tests {
|
||||
policy.next_poll(Duration::from_secs(30), true),
|
||||
Duration::from_millis(16)
|
||||
);
|
||||
assert_eq!(
|
||||
policy.next_poll(Duration::from_secs(30), false),
|
||||
Duration::from_secs(1)
|
||||
);
|
||||
assert_eq!(policy.next_poll(Duration::from_secs(30), false), Duration::from_secs(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -498,9 +454,7 @@ mod tests {
|
||||
.validate()
|
||||
.is_err()
|
||||
);
|
||||
assert!(
|
||||
crate::config::Config::parse_source("lock-screen { dim-after-seconds -1; }").is_err()
|
||||
);
|
||||
assert!(crate::config::Config::parse_source("lock-screen { dim-after-seconds -1; }").is_err());
|
||||
assert!(
|
||||
crate::config::Config::parse_source("lock-screen { sleep-after-seconds 90000; }")
|
||||
.unwrap()
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
use ferese_core::{OutputId, WorkspaceId};
|
||||
use ferese_layout::WindowId;
|
||||
use ferese_protocols::shell::v1::server::{
|
||||
ferese_shell_manager_v1::{self, FereseShellManagerV1},
|
||||
ferese_shell_v1::{self, FereseShellV1},
|
||||
};
|
||||
use smithay::{
|
||||
reexports::wayland_server::{
|
||||
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
|
||||
},
|
||||
wayland::{compositor::with_states, shell::xdg::XdgToplevelSurfaceData},
|
||||
};
|
||||
use ferese_protocols::shell::v1::server::ferese_shell_manager_v1::FereseShellManagerV1;
|
||||
use ferese_protocols::shell::v1::server::ferese_shell_v1::FereseShellV1;
|
||||
use ferese_protocols::shell::v1::server::{ferese_shell_manager_v1, ferese_shell_v1};
|
||||
use smithay::reexports::wayland_server::{Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource};
|
||||
use smithay::wayland::compositor::with_states;
|
||||
use smithay::wayland::shell::xdg::XdgToplevelSurfaceData;
|
||||
|
||||
use crate::{Ferese, private_client::ClientCapabilities, state::ClientState};
|
||||
use crate::Ferese;
|
||||
use crate::private_client::ClientCapabilities;
|
||||
use crate::state::ClientState;
|
||||
|
||||
pub(crate) fn init_global(display: &DisplayHandle) {
|
||||
display.create_global::<Ferese, FereseShellManagerV1, _>(4, ());
|
||||
@@ -52,11 +50,9 @@ impl GlobalDispatch<FereseShellManagerV1, ()> for Ferese {
|
||||
}
|
||||
|
||||
fn can_view(client: Client, _global_data: &()) -> bool {
|
||||
client.get_data::<ClientState>().is_some_and(|state| {
|
||||
state
|
||||
.capabilities
|
||||
.contains(ClientCapabilities::SHELL_CONTROL)
|
||||
})
|
||||
client
|
||||
.get_data::<ClientState>()
|
||||
.is_some_and(|state| state.capabilities.contains(ClientCapabilities::SHELL_CONTROL))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,19 +100,13 @@ impl Dispatch<FereseShellV1, ()> for Ferese {
|
||||
return;
|
||||
}
|
||||
match request {
|
||||
ferese_shell_v1::Request::ActivateWindow {
|
||||
window_hi,
|
||||
window_lo,
|
||||
} => state.handle_shell_window_request(
|
||||
ferese_shell_v1::Request::ActivateWindow { window_hi, window_lo } => state.handle_shell_window_request(
|
||||
shell,
|
||||
ferese_shell_v1::FailedRequest::ActivateWindow,
|
||||
join_id(window_hi, window_lo),
|
||||
ShellWindowAction::Activate,
|
||||
),
|
||||
ferese_shell_v1::Request::CloseWindow {
|
||||
window_hi,
|
||||
window_lo,
|
||||
} => state.handle_shell_window_request(
|
||||
ferese_shell_v1::Request::CloseWindow { window_hi, window_lo } => state.handle_shell_window_request(
|
||||
shell,
|
||||
ferese_shell_v1::FailedRequest::CloseWindow,
|
||||
join_id(window_hi, window_lo),
|
||||
@@ -129,11 +119,7 @@ impl Dispatch<FereseShellV1, ()> for Ferese {
|
||||
let workspace = WorkspaceId(join_u64(workspace_hi, workspace_lo));
|
||||
|
||||
if !state.activate_managed_workspace(workspace) {
|
||||
send_request_failed(
|
||||
shell,
|
||||
ferese_shell_v1::FailedRequest::ActivateWorkspace,
|
||||
workspace.0,
|
||||
);
|
||||
send_request_failed(shell, ferese_shell_v1::FailedRequest::ActivateWorkspace, workspace.0);
|
||||
}
|
||||
}
|
||||
ferese_shell_v1::Request::ConfirmLogout { serial } => {
|
||||
@@ -149,36 +135,21 @@ impl Dispatch<FereseShellV1, ()> for Ferese {
|
||||
force,
|
||||
} => {
|
||||
if force <= 1 {
|
||||
state.confirm_portal_logout(
|
||||
shell,
|
||||
serial,
|
||||
query_token,
|
||||
inhibitor_revision,
|
||||
force == 1,
|
||||
);
|
||||
state.confirm_portal_logout(shell, serial, query_token, inhibitor_revision, force == 1);
|
||||
}
|
||||
}
|
||||
ferese_shell_v1::Request::CancelLogout { serial } => {
|
||||
if state.logout_owner.as_ref() == Some(&shell.id())
|
||||
&& state.pending_logout == Some(serial)
|
||||
{
|
||||
if state.logout_owner.as_ref() == Some(&shell.id()) && state.pending_logout == Some(serial) {
|
||||
state.cancel_logout_confirmation();
|
||||
}
|
||||
}
|
||||
ferese_shell_v1::Request::EnterOverview => state.set_overview_active(true),
|
||||
ferese_shell_v1::Request::ExitOverview => state.set_overview_active(false),
|
||||
ferese_shell_v1::Request::SelectOverviewWindow {
|
||||
window_hi,
|
||||
window_lo,
|
||||
} => {
|
||||
ferese_shell_v1::Request::SelectOverviewWindow { window_hi, window_lo } => {
|
||||
let id = join_id(window_hi, window_lo);
|
||||
|
||||
if !state.select_overview_window(id) {
|
||||
send_request_failed(
|
||||
shell,
|
||||
ferese_shell_v1::FailedRequest::SelectOverviewWindow,
|
||||
id.0,
|
||||
);
|
||||
send_request_failed(shell, ferese_shell_v1::FailedRequest::SelectOverviewWindow, id.0);
|
||||
}
|
||||
}
|
||||
ferese_shell_v1::Request::Destroy => {
|
||||
@@ -202,12 +173,7 @@ impl Dispatch<FereseShellV1, ()> for Ferese {
|
||||
}
|
||||
}
|
||||
|
||||
fn logout_confirmation_matches(
|
||||
pending: Option<u32>,
|
||||
query: Option<u32>,
|
||||
serial: u32,
|
||||
token: u32,
|
||||
) -> bool {
|
||||
fn logout_confirmation_matches(pending: Option<u32>, query: Option<u32>, serial: u32, token: u32) -> bool {
|
||||
pending == Some(serial) && query == Some(token)
|
||||
}
|
||||
|
||||
@@ -221,14 +187,7 @@ fn consume_logout_confirmation(pending: &mut Option<u32>, serial: u32) -> bool {
|
||||
}
|
||||
|
||||
impl Ferese {
|
||||
fn confirm_portal_logout(
|
||||
&mut self,
|
||||
shell: &FereseShellV1,
|
||||
serial: u32,
|
||||
token: u32,
|
||||
revision: u32,
|
||||
force: bool,
|
||||
) {
|
||||
fn confirm_portal_logout(&mut self, shell: &FereseShellV1, serial: u32, token: u32, revision: u32, force: bool) {
|
||||
if self.logout_owner.as_ref() != Some(&shell.id())
|
||||
|| !logout_confirmation_matches(self.pending_logout, self.logout_query, serial, token)
|
||||
{
|
||||
@@ -269,9 +228,7 @@ impl Ferese {
|
||||
self.logout_query = Some(self.portal_session.begin_query());
|
||||
self.pending_logout = Some(serial);
|
||||
self.logout_owner = Some(shell.id());
|
||||
let output_name = self
|
||||
.focused_output()
|
||||
.map_or_else(String::new, |output| output.name());
|
||||
let output_name = self.focused_output().map_or_else(String::new, |output| output.name());
|
||||
shell.logout_requested(serial, output_name);
|
||||
}
|
||||
|
||||
@@ -280,11 +237,7 @@ impl Ferese {
|
||||
self.portal_session.cancel_query(token);
|
||||
}
|
||||
if let Some(serial) = self.pending_logout.take() {
|
||||
for shell in self
|
||||
.shell_resources
|
||||
.iter()
|
||||
.filter_map(|shell| shell.upgrade().ok())
|
||||
{
|
||||
for shell in self.shell_resources.iter().filter_map(|shell| shell.upgrade().ok()) {
|
||||
if self.logout_owner.as_ref() == Some(&shell.id()) && shell.version() >= 3 {
|
||||
shell.logout_cancelled(serial);
|
||||
}
|
||||
@@ -294,8 +247,7 @@ impl Ferese {
|
||||
}
|
||||
|
||||
pub(crate) fn send_shell_snapshots(&mut self) {
|
||||
self.shell_resources
|
||||
.retain(|resource| resource.upgrade().is_ok());
|
||||
self.shell_resources.retain(|resource| resource.upgrade().is_ok());
|
||||
if self.shell_resources.is_empty() {
|
||||
self.last_shell_snapshot = None;
|
||||
return;
|
||||
@@ -325,8 +277,7 @@ impl Ferese {
|
||||
// New subscribers always receive a complete snapshot. Do not update the
|
||||
// broadcast cache here: existing subscribers may still need this state.
|
||||
self.shell_snapshot_serial = self.shell_snapshot_serial.wrapping_add(1);
|
||||
self.shell_snapshot()
|
||||
.send(shell, self.shell_snapshot_serial);
|
||||
self.shell_snapshot().send(shell, self.shell_snapshot_serial);
|
||||
}
|
||||
|
||||
fn output_snapshots(&self) -> Vec<OutputSnapshot> {
|
||||
@@ -354,9 +305,8 @@ impl Ferese {
|
||||
.into_iter()
|
||||
.map(|workspace| {
|
||||
let output = self.output_workspaces.output_for_workspace(workspace.id);
|
||||
let active = output.is_some_and(|output| {
|
||||
self.output_workspaces.active_workspace(output) == Some(workspace.id)
|
||||
});
|
||||
let active =
|
||||
output.is_some_and(|output| self.output_workspaces.active_workspace(output) == Some(workspace.id));
|
||||
|
||||
WorkspaceSnapshot {
|
||||
id: workspace.id,
|
||||
@@ -479,10 +429,7 @@ impl ShellSnapshot {
|
||||
}
|
||||
for workspace in &self.workspaces {
|
||||
let (workspace_hi, workspace_lo) = split_id(workspace.id.0);
|
||||
let (output_hi, output_lo) = workspace
|
||||
.output
|
||||
.map(|output| split_id(output.0))
|
||||
.unwrap_or_default();
|
||||
let (output_hi, output_lo) = workspace.output.map(|output| split_id(output.0)).unwrap_or_default();
|
||||
|
||||
shell.workspace(
|
||||
workspace_hi,
|
||||
@@ -564,12 +511,7 @@ mod tests {
|
||||
#[test]
|
||||
fn logout_serial_cannot_authorize_a_different_session_query() {
|
||||
assert!(super::logout_confirmation_matches(Some(42), Some(9), 42, 9));
|
||||
assert!(!super::logout_confirmation_matches(
|
||||
Some(42),
|
||||
Some(9),
|
||||
42,
|
||||
10
|
||||
));
|
||||
assert!(!super::logout_confirmation_matches(Some(42), Some(9), 42, 10));
|
||||
assert!(!super::logout_confirmation_matches(None, Some(9), 42, 9));
|
||||
}
|
||||
|
||||
@@ -633,11 +575,7 @@ mod tests {
|
||||
fn config_chunks_are_bounded_and_preserve_unicode() {
|
||||
let source = "abc🌲é".repeat(9000);
|
||||
let chunks = config_chunks(&source);
|
||||
assert!(
|
||||
chunks
|
||||
.iter()
|
||||
.all(|chunk| !chunk.is_empty() && chunk.len() <= 1024)
|
||||
);
|
||||
assert!(chunks.iter().all(|chunk| !chunk.is_empty() && chunk.len() <= 1024));
|
||||
assert_eq!(chunks.concat(), source);
|
||||
assert!(config_chunks("").is_empty());
|
||||
}
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
use ferese_layout::WindowId;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub(crate) fn layer_priority(
|
||||
floating: bool,
|
||||
zooming: bool,
|
||||
fullscreen: bool,
|
||||
above_fullscreen: bool,
|
||||
) -> u8 {
|
||||
use ferese_layout::WindowId;
|
||||
|
||||
pub(crate) fn layer_priority(floating: bool, zooming: bool, fullscreen: bool, above_fullscreen: bool) -> u8 {
|
||||
if above_fullscreen {
|
||||
4
|
||||
} else if fullscreen {
|
||||
@@ -97,18 +93,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn floating_windows_stay_above_tiled_and_maximized_windows() {
|
||||
assert!(
|
||||
layer_priority(true, false, false, false) > layer_priority(false, true, false, false)
|
||||
);
|
||||
assert!(
|
||||
layer_priority(false, true, false, false) > layer_priority(false, false, false, false)
|
||||
);
|
||||
assert!(
|
||||
layer_priority(false, false, true, false) > layer_priority(true, false, false, false)
|
||||
);
|
||||
assert!(
|
||||
layer_priority(true, false, false, true) > layer_priority(false, false, true, false)
|
||||
);
|
||||
assert!(layer_priority(true, false, false, false) > layer_priority(false, true, false, false));
|
||||
assert!(layer_priority(false, true, false, false) > layer_priority(false, false, false, false));
|
||||
assert!(layer_priority(false, false, true, false) > layer_priority(true, false, false, false));
|
||||
assert!(layer_priority(true, false, false, true) > layer_priority(false, false, true, false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+284
-534
File diff suppressed because it is too large.
Load diff
+36
-84
@@ -1,28 +1,22 @@
|
||||
//! One decoded image and one texture per GPU context, not double-buffered
|
||||
//! full-screen UI surfaces per monitor. Resize changes sampling only.
|
||||
use crate::presentation::NativeTextureElement;
|
||||
use std::cell::Cell;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::rc::Rc;
|
||||
use std::sync::mpsc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use serde::Deserialize;
|
||||
use smithay::{
|
||||
backend::{
|
||||
allocator::Fourcc,
|
||||
renderer::{
|
||||
ErasedContextId, ImportMem, Renderer,
|
||||
element::Id,
|
||||
gles::{GlesRenderer, GlesTexture},
|
||||
utils::CommitCounter,
|
||||
},
|
||||
},
|
||||
output::Output,
|
||||
utils::{Buffer, Physical, Rectangle, Size},
|
||||
};
|
||||
use std::{
|
||||
cell::Cell,
|
||||
collections::HashMap,
|
||||
path::PathBuf,
|
||||
rc::Rc,
|
||||
sync::mpsc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use smithay::backend::allocator::Fourcc;
|
||||
use smithay::backend::renderer::element::Id;
|
||||
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture};
|
||||
use smithay::backend::renderer::utils::CommitCounter;
|
||||
use smithay::backend::renderer::{ErasedContextId, ImportMem, Renderer};
|
||||
use smithay::output::Output;
|
||||
use smithay::utils::{Buffer, Physical, Rectangle, Size};
|
||||
|
||||
use crate::presentation::NativeTextureElement;
|
||||
|
||||
const UPLOAD_RETRY_DELAY: Duration = Duration::from_secs(5);
|
||||
|
||||
@@ -80,10 +74,7 @@ impl WallpaperState {
|
||||
Self::with_wakeup(config, None)
|
||||
}
|
||||
|
||||
pub fn with_wakeup(
|
||||
config: WallpaperConfig,
|
||||
wakeup: Option<smithay::reexports::calloop::LoopSignal>,
|
||||
) -> Self {
|
||||
pub fn with_wakeup(config: WallpaperConfig, wakeup: Option<smithay::reexports::calloop::LoopSignal>) -> Self {
|
||||
let retained = config.clone();
|
||||
let (sender, receiver) = mpsc::channel();
|
||||
let owned = config.path.as_ref().is_some_and(|path| path.is_file());
|
||||
@@ -92,8 +83,7 @@ impl WallpaperState {
|
||||
let wakeup = wakeup.clone();
|
||||
std::thread::spawn(move || {
|
||||
let load = || -> Result<image::RgbaImage, String> {
|
||||
let (width, height) =
|
||||
image::image_dimensions(&path).map_err(|e| e.to_string())?;
|
||||
let (width, height) = image::image_dimensions(&path).map_err(|e| e.to_string())?;
|
||||
if u64::from(width) * u64::from(height) * 4 > 256 * 1024 * 1024 {
|
||||
return Err("wallpaper exceeds the 256 MiB decode limit".into());
|
||||
}
|
||||
@@ -164,9 +154,7 @@ impl WallpaperState {
|
||||
}
|
||||
|
||||
fn upload_ready(&self, context: &ErasedContextId, now: Instant) -> bool {
|
||||
self.upload_retries
|
||||
.get(context)
|
||||
.is_none_or(|retry| now >= *retry)
|
||||
self.upload_retries.get(context).is_none_or(|retry| now >= *retry)
|
||||
}
|
||||
|
||||
pub(crate) fn take_retry_wakeup(&self) -> bool {
|
||||
@@ -250,11 +238,7 @@ impl WallpaperState {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn element(
|
||||
&mut self,
|
||||
renderer: &mut GlesRenderer,
|
||||
output: &Output,
|
||||
) -> Option<NativeTextureElement> {
|
||||
pub fn element(&mut self, renderer: &mut GlesRenderer, output: &Output) -> Option<NativeTextureElement> {
|
||||
let pixels = self.pixels.as_ref()?;
|
||||
let context = renderer.context_id().erased();
|
||||
if !self.textures.contains_key(&context) {
|
||||
@@ -265,32 +249,21 @@ impl WallpaperState {
|
||||
match renderer.import_memory(pixels.as_raw(), Fourcc::Abgr8888, size, false) {
|
||||
Ok(texture) => {
|
||||
self.upload_retries.remove(&context);
|
||||
tracing::debug!(
|
||||
bytes = pixels.as_raw().len(),
|
||||
"uploaded shared wallpaper texture"
|
||||
);
|
||||
self.textures.insert(
|
||||
context.clone(),
|
||||
WallpaperTexture {
|
||||
texture,
|
||||
id: Id::new(),
|
||||
},
|
||||
);
|
||||
tracing::debug!(bytes = pixels.as_raw().len(), "uploaded shared wallpaper texture");
|
||||
self.textures
|
||||
.insert(context.clone(), WallpaperTexture { texture, id: Id::new() });
|
||||
}
|
||||
Err(error) => {
|
||||
// Avoid retrying a large failed allocation every frame, but
|
||||
// allow recovery from temporary GPU memory pressure.
|
||||
self.upload_retries
|
||||
.insert(context, Instant::now() + UPLOAD_RETRY_DELAY);
|
||||
self.upload_retries.insert(context, Instant::now() + UPLOAD_RETRY_DELAY);
|
||||
tracing::debug!(%error, "wallpaper texture import failed");
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
let cached = self.textures.get(&context)?;
|
||||
let output_size = output
|
||||
.current_transform()
|
||||
.transform_size(output.current_mode()?.size);
|
||||
let output_size = output.current_transform().transform_size(output.current_mode()?.size);
|
||||
let (geometry, source) = image_geometry(
|
||||
(pixels.width() as i32, pixels.height() as i32).into(),
|
||||
output_size,
|
||||
@@ -324,11 +297,7 @@ fn image_geometry(
|
||||
(
|
||||
Rectangle::from_size(output),
|
||||
Rectangle::new(
|
||||
(
|
||||
(f64::from(image.w) - width) / 2.0,
|
||||
(f64::from(image.h) - height) / 2.0,
|
||||
)
|
||||
.into(),
|
||||
((f64::from(image.w) - width) / 2.0, (f64::from(image.h) - height) / 2.0).into(),
|
||||
(width, height).into(),
|
||||
),
|
||||
)
|
||||
@@ -341,10 +310,7 @@ fn image_geometry(
|
||||
)
|
||||
.into();
|
||||
(
|
||||
Rectangle::new(
|
||||
((output.w - size.w) / 2, (output.h - size.h) / 2).into(),
|
||||
size,
|
||||
),
|
||||
Rectangle::new(((output.w - size.w) / 2, (output.h - size.h) / 2).into(), size),
|
||||
Rectangle::from_size(image.to_f64()),
|
||||
)
|
||||
}
|
||||
@@ -373,9 +339,7 @@ mod tests {
|
||||
state.arm_retry_timer(&event_loop.handle(), now).unwrap();
|
||||
assert!(state.retry_timer_pending.get());
|
||||
assert!(!state.take_retry_wakeup());
|
||||
event_loop
|
||||
.dispatch(Some(Duration::from_secs(1)), &mut ())
|
||||
.unwrap();
|
||||
event_loop.dispatch(Some(Duration::from_secs(1)), &mut ()).unwrap();
|
||||
assert!(!state.retry_timer_pending.get());
|
||||
assert!(
|
||||
state.take_retry_wakeup(),
|
||||
@@ -383,10 +347,7 @@ mod tests {
|
||||
);
|
||||
assert!(!state.take_retry_wakeup());
|
||||
let after = Instant::now();
|
||||
assert_eq!(
|
||||
state.next_retry_deadline(after),
|
||||
Some(after + UPLOAD_RETRY_DELAY)
|
||||
);
|
||||
assert_eq!(state.next_retry_deadline(after), Some(after + UPLOAD_RETRY_DELAY));
|
||||
state.forget_context(&context);
|
||||
assert_eq!(state.next_retry_deadline(after), None);
|
||||
state.retry_wakeup.set(true);
|
||||
@@ -398,7 +359,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn decoder_wakes_an_idle_event_loop() {
|
||||
use smithay::reexports::calloop::{EventLoop, timer::Timer};
|
||||
use smithay::reexports::calloop::EventLoop;
|
||||
use smithay::reexports::calloop::timer::Timer;
|
||||
let mut event_loop = EventLoop::<WallpaperState>::try_new().unwrap();
|
||||
event_loop
|
||||
.handle()
|
||||
@@ -407,8 +369,7 @@ mod tests {
|
||||
})
|
||||
.unwrap();
|
||||
let signal = event_loop.get_signal();
|
||||
let mut state =
|
||||
WallpaperState::with_wakeup(WallpaperConfig::default(), Some(signal.clone()));
|
||||
let mut state = WallpaperState::with_wakeup(WallpaperConfig::default(), Some(signal.clone()));
|
||||
event_loop
|
||||
.run(None, &mut state, |state| {
|
||||
if state.poll() {
|
||||
@@ -429,18 +390,14 @@ mod tests {
|
||||
mode: WallpaperMode::Fill,
|
||||
});
|
||||
let now = Instant::now();
|
||||
state
|
||||
.upload_retries
|
||||
.insert(context.clone(), now + UPLOAD_RETRY_DELAY);
|
||||
state.upload_retries.insert(context.clone(), now + UPLOAD_RETRY_DELAY);
|
||||
assert!(!state.upload_ready(&context, now));
|
||||
assert!(state.upload_ready(&other, now));
|
||||
assert!(state.upload_ready(&context, now + UPLOAD_RETRY_DELAY));
|
||||
state.forget_context(&context);
|
||||
assert!(state.upload_ready(&context, now));
|
||||
|
||||
state
|
||||
.upload_retries
|
||||
.insert(context.clone(), now + UPLOAD_RETRY_DELAY);
|
||||
state.upload_retries.insert(context.clone(), now + UPLOAD_RETRY_DELAY);
|
||||
let (sender, receiver) = mpsc::channel();
|
||||
state.receiver = Some(receiver);
|
||||
sender.send(Ok(image::RgbaImage::new(2, 2))).unwrap();
|
||||
@@ -489,16 +446,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn fill_crops_and_fit_letterboxes_without_reallocating_image() {
|
||||
let (geometry, source) = image_geometry(
|
||||
(3840, 2160).into(),
|
||||
(1600, 1000).into(),
|
||||
WallpaperMode::Fill,
|
||||
);
|
||||
let (geometry, source) = image_geometry((3840, 2160).into(), (1600, 1000).into(), WallpaperMode::Fill);
|
||||
assert_eq!(geometry.size, (1600, 1000).into());
|
||||
assert_eq!(source.size.h, 2160.0);
|
||||
assert!(source.loc.x > 0.0);
|
||||
let (geometry, source) =
|
||||
image_geometry((3840, 2160).into(), (1600, 1000).into(), WallpaperMode::Fit);
|
||||
let (geometry, source) = image_geometry((3840, 2160).into(), (1600, 1000).into(), WallpaperMode::Fit);
|
||||
assert_eq!(geometry.size, (1600, 900).into());
|
||||
assert_eq!(geometry.loc, (0, 50).into());
|
||||
assert_eq!(source.size, (3840.0, 2160.0).into());
|
||||
|
||||
@@ -63,12 +63,7 @@ pub fn validate(configured: &[WindowRuleConfig]) -> Result<Vec<WindowRule>, Stri
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn resolve(
|
||||
rules: &[WindowRule],
|
||||
app_id: Option<&str>,
|
||||
title: Option<&str>,
|
||||
transient: bool,
|
||||
) -> WindowRuleResult {
|
||||
pub fn resolve(rules: &[WindowRule], app_id: Option<&str>, title: Option<&str>, transient: bool) -> WindowRuleResult {
|
||||
let app_id = app_id.map(normalize_app_id);
|
||||
let mut result = WindowRuleResult::default();
|
||||
if matches!(
|
||||
@@ -88,10 +83,7 @@ pub fn resolve(
|
||||
.app_id
|
||||
.as_deref()
|
||||
.is_none_or(|expected| app_id.as_deref() == Some(expected));
|
||||
let title_matches = rule
|
||||
.title
|
||||
.as_deref()
|
||||
.is_none_or(|expected| title == Some(expected));
|
||||
let title_matches = rule.title.as_deref().is_none_or(|expected| title == Some(expected));
|
||||
let transient_matches = rule.transient.is_none_or(|expected| expected == transient);
|
||||
if !app_id_matches || !title_matches || !transient_matches {
|
||||
continue;
|
||||
@@ -109,14 +101,10 @@ pub fn resolve(
|
||||
|
||||
fn validate_rule(index: usize, rule: &WindowRuleConfig) -> Result<WindowRule, String> {
|
||||
if rule.app_id.is_none() && rule.title.is_none() && rule.transient.is_none() {
|
||||
return Err(format!(
|
||||
"window_rules[{index}] must match app_id, title, or transient"
|
||||
));
|
||||
return Err(format!("window_rules[{index}] must match app_id, title, or transient"));
|
||||
}
|
||||
if rule.workspace == Some(0) {
|
||||
return Err(format!(
|
||||
"window_rules[{index}].workspace must be greater than zero"
|
||||
));
|
||||
return Err(format!("window_rules[{index}].workspace must be greater than zero"));
|
||||
}
|
||||
|
||||
let app_id = rule
|
||||
@@ -157,11 +145,7 @@ fn nonempty_matcher(value: &str) -> Result<&str, &'static str> {
|
||||
}
|
||||
}
|
||||
|
||||
fn positive_dimension(
|
||||
value: Option<f64>,
|
||||
index: usize,
|
||||
field: &'static str,
|
||||
) -> Result<Option<f64>, String> {
|
||||
fn positive_dimension(value: Option<f64>, index: usize, field: &'static str) -> Result<Option<f64>, String> {
|
||||
if value.is_none_or(|value| value.is_finite() && value > 0.0) {
|
||||
Ok(value)
|
||||
} else {
|
||||
@@ -193,9 +177,7 @@ mod tests {
|
||||
assert_eq!(removed.floating, Some(false));
|
||||
assert_eq!(removed.fullscreen, Some(false));
|
||||
assert_eq!(
|
||||
super::live_result(old, Default::default(), true)
|
||||
.unwrap()
|
||||
.floating,
|
||||
super::live_result(old, Default::default(), true).unwrap().floating,
|
||||
Some(true)
|
||||
);
|
||||
let new = super::WindowRuleResult {
|
||||
@@ -234,15 +216,9 @@ mod tests {
|
||||
..config(app_id)
|
||||
}])
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
resolve(&rules, Some(app_id), None, false).floating,
|
||||
Some(false)
|
||||
);
|
||||
assert_eq!(resolve(&rules, Some(app_id), None, false).floating, Some(false));
|
||||
}
|
||||
assert_eq!(
|
||||
resolve(&[], Some("org.example.Editor"), None, false).floating,
|
||||
None
|
||||
);
|
||||
assert_eq!(resolve(&[], Some("org.example.Editor"), None, false).floating, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+208
-501
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user