Fix compositor output routing and pointer hints; format imports

This commit is contained in:
Samuel Onoja committed 2026-09-30 06:39:58 +01:00
1 parent 88e4f3c135
commit 46509f880a
108 files changed
+3682 -8371

No files matched your search

+113 -281
View File
@@ -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);
}
+10 -33
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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));
}
}
+5 -7
View File
@@ -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
View File
@@ -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());
}
}
+9 -22
View File
@@ -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);
}
+23 -69
View File
@@ -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))
}
+5 -12
View File
@@ -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;
}
+17 -30
View File
@@ -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));
+6 -14
View File
@@ -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();
};
+16 -41
View File
@@ -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)
}
+38 -57
View File
@@ -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,
});
}
}
+27 -51
View File
@@ -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]
+56 -212
View File
@@ -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(&region(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(&region(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(&region(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(&region(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(&region(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 {
+10 -25
View File
@@ -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();
+27 -66
View File
@@ -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
})
+83 -72
View File
@@ -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
View File
@@ -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;
+20 -81
View File
@@ -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
View File
@@ -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
View File
@@ -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);
}
}
+7 -22
View File
@@ -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
View File
@@ -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,
);
+37 -95
View File
@@ -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);
+18 -46
View File
@@ -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
View File
@@ -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);
}
}
+12 -20
View File
@@ -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
View File
@@ -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);
}
}
+1 -2
View File
@@ -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));
}
}
+36 -82
View File
@@ -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()
+30 -92
View File
@@ -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());
}
+7 -19
View File
@@ -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
View File
File diff suppressed because it is too large. Load diff
+36 -84
View File
@@ -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());
+8 -32
View File
@@ -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
View File
File diff suppressed because it is too large. Load diff