Format Rust sources at 100 columns

This commit is contained in:
Samuel Onoja committed 2026-10-22 01:28:03 +01:00
1 parent 4661f595f8
commit 610478f8c7
336 files changed
+23497 -7892

No files matched your search

+89 -29
View File
@@ -48,10 +48,13 @@ impl Default for Engine {
pomodoro: None, pomodoro: None,
pomodoro_settings: Default::default(), pomodoro_settings: Default::default(),
alerts: None, alerts: None,
store: Store::new(ferese_ipc::activities::now_ms() ^ u64::from(std::process::id()) << 32), store: Store::new(
ferese_ipc::activities::now_ms() ^ u64::from(std::process::id()) << 32,
),
rules: Arc::new(RwLock::new(Arc::new(Rules { rules: Arc::new(RwLock::new(Arc::new(Rules {
enabled: false, enabled: false,
matcher: ferese_config::activities::Matcher::new(&settings).expect("built-in rules"), matcher: ferese_config::activities::Matcher::new(&settings)
.expect("built-in rules"),
}))), }))),
monitor: None, monitor: None,
process_enabled: false, process_enabled: false,
@@ -141,7 +144,9 @@ impl Engine {
{ {
match crate::activities_process::Watcher::start(self.rules.clone(), sender.clone()) { match crate::activities_process::Watcher::start(self.rules.clone(), sender.clone()) {
Ok(watcher) => self.process_watcher = Some(watcher), Ok(watcher) => self.process_watcher = Some(watcher),
Err(error) => tracing::warn!(%error, "could not start optional activity process source"), Err(error) => {
tracing::warn!(%error, "could not start optional activity process source")
}
} }
} }
} }
@@ -160,17 +165,22 @@ impl Engine {
.cloned() .cloned()
.chain(self.pomodoro.iter().map(|timer| timer.activity())) .chain(self.pomodoro.iter().map(|timer| timer.activity()))
.collect(), .collect(),
pomodoro: self.pomodoro.as_ref().map(|timer| ferese_ipc::activities::Pomodoro { pomodoro: self
id: timer.id.clone(), .pomodoro
timer: timer.session.clone(), .as_ref()
}), .map(|timer| ferese_ipc::activities::Pomodoro {
id: timer.id.clone(),
timer: timer.session.clone(),
}),
windows: self windows: self
.windows .windows
.iter() .iter()
.map(|(activity, window)| ferese_ipc::activities::ActivityWindow { .map(
activity: activity.clone(), |(activity, window)| ferese_ipc::activities::ActivityWindow {
window: *window, activity: activity.clone(),
}) window: *window,
},
)
.collect(), .collect(),
}); });
self.cached_snapshot = Some(snapshot.clone()); self.cached_snapshot = Some(snapshot.clone());
@@ -206,7 +216,13 @@ impl Engine {
pub fn get(&mut self, request: u64, sender: SyncSender<Response>) { pub fn get(&mut self, request: u64, sender: SyncSender<Response>) {
self.deliver(vec![(request, sender)]); self.deliver(vec![(request, sender)]);
} }
pub fn watch(&mut self, connection: u64, request: u64, since: u64, sender: SyncSender<Response>) { pub fn watch(
&mut self,
connection: u64,
request: u64,
since: u64,
sender: SyncSender<Response>,
) {
if self.locked || since != self.revision { if self.locked || since != self.revision {
self.get(request, sender); self.get(request, sender);
} else if self.watchers.len() < 32 { } else if self.watchers.len() < 32 {
@@ -258,7 +274,11 @@ impl Engine {
self.store self.store
.next_deadline(now) .next_deadline(now)
.into_iter() .into_iter()
.chain(self.pomodoro.as_ref().and_then(|timer| timer.session.deadline)) .chain(
self.pomodoro
.as_ref()
.and_then(|timer| timer.session.deadline),
)
.min(), .min(),
) { ) {
tracing::error!(%error, "Activity deadline could not be armed"); tracing::error!(%error, "Activity deadline could not be armed");
@@ -307,16 +327,17 @@ impl Engine {
window: Option<u64>, window: Option<u64>,
) -> Result<serde_json::Value, &'static str> { ) -> Result<serde_json::Value, &'static str> {
let started = match &publish { let started = match &publish {
Publish::Start { local, .. } | Publish::Run { local, .. } | Publish::Hook { local, .. } => { Publish::Start { local, .. }
Some(local.clone()) | Publish::Run { local, .. }
} | Publish::Hook { local, .. } => Some(local.clone()),
_ => None, _ => None,
}; };
let now = ferese_ipc::activities::now_ms(); let now = ferese_ipc::activities::now_ms();
if matches!(publish, Publish::Hook { .. }) && matched.is_none() { if matches!(publish, Publish::Hook { .. }) && matched.is_none() {
return Ok(json!({"matched":false})); return Ok(json!({"matched":false}));
} }
self.rates.retain(|_, (at, _)| now.saturating_sub(*at) < 60_000); self.rates
.retain(|_, (at, _)| now.saturating_sub(*at) < 60_000);
if self.rates.len() >= 128 && !self.rates.contains_key(&owner) { if self.rates.len() >= 128 && !self.rates.contains_key(&owner) {
return Err("owner publication quota exceeded"); return Err("owner publication quota exceeded");
} }
@@ -392,7 +413,9 @@ impl Engine {
value: Value::Indeterminate, value: Value::Indeterminate,
template: Template::Progress, template: Template::Progress,
}; };
let id = self.store.start(owner, local, data, now, 0, None, true, true, None)?; let id = self
.store
.start(owner, local, data, now, 0, None, true, true, None)?;
json!({"handle":id,"progress":"indeterminate"}) json!({"handle":id,"progress":"indeterminate"})
} }
} }
@@ -486,7 +509,10 @@ impl Engine {
} }
impl crate::Ferese { impl crate::Ferese {
pub(crate) fn activity_window(&self, owner: &crate::ipc::activity_owner::VerifiedOwner) -> Option<u64> { pub(crate) fn activity_window(
&self,
owner: &crate::ipc::activity_owner::VerifiedOwner,
) -> Option<u64> {
use smithay::reexports::wayland_server::Resource; use smithay::reexports::wayland_server::Resource;
let mut ancestors = owner.ancestor_pids().peekable(); let mut ancestors = owner.ancestor_pids().peekable();
ancestors.peek()?; ancestors.peek()?;
@@ -495,7 +521,8 @@ impl crate::Ferese {
.records() .records()
.filter_map(|(id, record)| { .filter_map(|(id, record)| {
let client = record.surface.as_ref()?.client()?; let client = record.surface.as_ref()?.client()?;
let pid = u32::try_from(client.get_credentials(&self.display_handle).ok()?.pid).ok()?; let pid =
u32::try_from(client.get_credentials(&self.display_handle).ok()?.pid).ok()?;
Some((pid, id.0)) Some((pid, id.0))
}) })
.collect(); .collect();
@@ -546,15 +573,27 @@ mod tests {
for since in [revision, engine.revision] { for since in [revision, engine.revision] {
let (send, receive) = std::sync::mpsc::sync_channel(1); let (send, receive) = std::sync::mpsc::sync_channel(1);
engine.watch(2, 43, since, send); engine.watch(2, 43, since, send);
assert_eq!(receive.recv().unwrap().error.unwrap().code, "session_locked"); assert_eq!(
receive.recv().unwrap().error.unwrap().code,
"session_locked"
);
} }
let (send, receive) = std::sync::mpsc::sync_channel(1); let (send, receive) = std::sync::mpsc::sync_channel(1);
engine.get(44, send); engine.get(44, send);
assert_eq!(receive.recv().unwrap().error.unwrap().code, "session_locked"); assert_eq!(
receive.recv().unwrap().error.unwrap().code,
"session_locked"
);
engine.set_locked(false); engine.set_locked(false);
let (send, receive) = std::sync::mpsc::sync_channel(1); let (send, receive) = std::sync::mpsc::sync_channel(1);
engine.watch(3, 45, revision, send); engine.watch(3, 45, revision, send);
assert!(receive.recv_timeout(Duration::from_secs(2)).unwrap().result.is_some()); assert!(
receive
.recv_timeout(Duration::from_secs(2))
.unwrap()
.result
.is_some()
);
} }
#[test] #[test]
@@ -605,7 +644,10 @@ mod tests {
fn snapshots_are_lazy_shared_and_immutable_across_publications() { fn snapshots_are_lazy_shared_and_immutable_across_publications() {
let mut engine = Engine::default(); let mut engine = Engine::default();
engine.changed(); engine.changed();
assert!(engine.cached_snapshot.is_none(), "no subscriber needs a snapshot"); assert!(
engine.cached_snapshot.is_none(),
"no subscriber needs a snapshot"
);
let before = engine.snapshot(); let before = engine.snapshot();
assert!(Arc::ptr_eq(&before, &engine.snapshot())); assert!(Arc::ptr_eq(&before, &engine.snapshot()));
let (first, first_reply) = std::sync::mpsc::sync_channel(1); let (first, first_reply) = std::sync::mpsc::sync_channel(1);
@@ -637,13 +679,19 @@ mod tests {
} }
let (sender, receiver) = std::sync::mpsc::sync_channel(1); let (sender, receiver) = std::sync::mpsc::sync_channel(1);
engine.watch(3, 33, before.revision, sender); engine.watch(3, 33, before.revision, sender);
assert_eq!(receiver.recv_timeout(Duration::from_secs(2)).unwrap().id, 33); assert_eq!(
receiver.recv_timeout(Duration::from_secs(2)).unwrap().id,
33
);
assert!(Arc::ptr_eq(&after, &engine.snapshot())); assert!(Arc::ptr_eq(&after, &engine.snapshot()));
} }
#[test] #[test]
fn process_that_exits_before_detection_dispatch_is_not_published() { fn process_that_exits_before_detection_dispatch_is_not_published() {
let mut child = std::process::Command::new("sleep").arg("30").spawn().unwrap(); let mut child = std::process::Command::new("sleep")
.arg("30")
.spawn()
.unwrap();
let (owner, fd, _) = crate::ipc::activity_owner::inspect(child.id()).unwrap(); let (owner, fd, _) = crate::ipc::activity_owner::inspect(child.id()).unwrap();
let matched = ferese_config::activities::Matcher::new(&Default::default()) let matched = ferese_config::activities::Matcher::new(&Default::default())
.unwrap() .unwrap()
@@ -681,7 +729,10 @@ mod tests {
#[test] #[test]
fn terminal_matching_uses_nearest_unique_ancestor_and_never_guesses() { fn terminal_matching_uses_nearest_unique_ancestor_and_never_guesses() {
let ancestry = [400, 300, 200, 100]; let ancestry = [400, 300, 200, 100];
assert_eq!(origin_window(ancestry.into_iter(), &[(200, 7), (100, 8)]), Some(7)); assert_eq!(
origin_window(ancestry.into_iter(), &[(200, 7), (100, 8)]),
Some(7)
);
assert_eq!( assert_eq!(
origin_window(ancestry.into_iter(), &[(200, 7), (200, 9), (100, 8)]), origin_window(ancestry.into_iter(), &[(200, 7), (200, 9), (100, 8)]),
None None
@@ -739,7 +790,9 @@ mod tests {
.unwrap(); .unwrap();
engine.owner_exit(owner()); engine.owner_exit(owner());
assert_eq!(engine.snapshot().windows[0].activity, id); assert_eq!(engine.snapshot().windows[0].activity, id);
engine.store.advance(ferese_ipc::activities::now_ms() + 2_000); engine
.store
.advance(ferese_ipc::activities::now_ms() + 2_000);
engine.changed(); engine.changed();
assert!(engine.snapshot().windows.is_empty()); assert!(engine.snapshot().windows.is_empty());
} }
@@ -776,7 +829,14 @@ mod tests {
); );
} }
assert_eq!( assert_eq!(
engine.store.entries().next().unwrap().data.output.as_deref(), engine
.store
.entries()
.next()
.unwrap()
.data
.output
.as_deref(),
Some("first") Some("first")
); );
engine engine
+21 -7
View File
@@ -1,6 +1,8 @@
//! Activity deadlines share CLOCK_BOOTTIME with the published countdowns. A //! Activity deadlines share CLOCK_BOOTTIME with the published countdowns. A
//! monotonic calloop timer would otherwise keep waiting for time spent asleep. //! monotonic calloop timer would otherwise keep waiting for time spent asleep.
use smithay::reexports::calloop::{Interest, LoopHandle, Mode, PostAction, RegistrationToken, generic::Generic}; use smithay::reexports::calloop::{
Interest, LoopHandle, Mode, PostAction, RegistrationToken, generic::Generic,
};
use std::{ use std::{
io, io,
os::fd::{AsRawFd, FromRawFd, OwnedFd}, os::fd::{AsRawFd, FromRawFd, OwnedFd},
@@ -13,9 +15,13 @@ pub(crate) struct Deadline {
token: RegistrationToken, token: RegistrationToken,
} }
impl Deadline { impl Deadline {
pub fn new(handle: LoopHandle<'static, crate::Ferese>) -> Result<Self, Box<dyn std::error::Error>> { pub fn new(
handle: LoopHandle<'static, crate::Ferese>,
) -> Result<Self, Box<dyn std::error::Error>> {
// SAFETY: timerfd_create returns a new owned descriptor or -1. // SAFETY: timerfd_create returns a new owned descriptor or -1.
let fd = unsafe { libc::timerfd_create(libc::CLOCK_BOOTTIME, libc::TFD_CLOEXEC | libc::TFD_NONBLOCK) }; let fd = unsafe {
libc::timerfd_create(libc::CLOCK_BOOTTIME, libc::TFD_CLOEXEC | libc::TFD_NONBLOCK)
};
if fd < 0 { if fd < 0 {
return Err(io::Error::last_os_error().into()); return Err(io::Error::last_os_error().into());
} }
@@ -25,7 +31,8 @@ impl Deadline {
|_, fd, state| { |_, fd, state| {
let mut expirations = 0_u64; let mut expirations = 0_u64;
// SAFETY: expirations is a writable eight-byte timerfd counter. // SAFETY: expirations is a writable eight-byte timerfd counter.
let result = unsafe { libc::read(fd.as_raw_fd(), (&raw mut expirations).cast(), 8) }; let result =
unsafe { libc::read(fd.as_raw_fd(), (&raw mut expirations).cast(), 8) };
if result < 0 { if result < 0 {
let error = io::Error::last_os_error(); let error = io::Error::last_os_error();
if error.kind() != io::ErrorKind::WouldBlock { if error.kind() != io::ErrorKind::WouldBlock {
@@ -50,7 +57,10 @@ impl Deadline {
} }
let ms = deadline.unwrap_or(0); let ms = deadline.unwrap_or(0);
let spec = libc::itimerspec { let spec = libc::itimerspec {
it_interval: libc::timespec { tv_sec: 0, tv_nsec: 0 }, it_interval: libc::timespec {
tv_sec: 0,
tv_nsec: 0,
},
it_value: libc::timespec { it_value: libc::timespec {
tv_sec: (ms / 1_000) as _, tv_sec: (ms / 1_000) as _,
tv_nsec: ((ms % 1_000) * 1_000_000) as _, tv_nsec: ((ms % 1_000) * 1_000_000) as _,
@@ -86,13 +96,17 @@ mod tests {
fn deadline_uses_boottime_and_disarming_leaves_no_periodic_wakeups() { fn deadline_uses_boottime_and_disarming_leaves_no_periodic_wakeups() {
let events = smithay::reexports::calloop::EventLoop::<crate::Ferese>::try_new().unwrap(); let events = smithay::reexports::calloop::EventLoop::<crate::Ferese>::try_new().unwrap();
let deadline = Deadline::new(events.handle()).unwrap(); let deadline = Deadline::new(events.handle()).unwrap();
let info = std::fs::read_to_string(format!("/proc/self/fdinfo/{}", deadline.fd.as_raw_fd())).unwrap(); let info =
std::fs::read_to_string(format!("/proc/self/fdinfo/{}", deadline.fd.as_raw_fd()))
.unwrap();
assert!( assert!(
info.lines() info.lines()
.any(|line| line.split_whitespace().collect::<Vec<_>>() == ["clockid:", "7"]), .any(|line| line.split_whitespace().collect::<Vec<_>>() == ["clockid:", "7"]),
"{info}" "{info}"
); );
deadline.update(Some(ferese_ipc::activities::now_ms() + 20)).unwrap(); deadline
.update(Some(ferese_ipc::activities::now_ms() + 20))
.unwrap();
let mut pollfd = libc::pollfd { let mut pollfd = libc::pollfd {
fd: deadline.fd.as_raw_fd(), fd: deadline.fd.as_raw_fd(),
events: libc::POLLIN, events: libc::POLLIN,
+30 -10
View File
@@ -69,7 +69,9 @@ impl Engine {
return Err("Pomodoro session changed; refresh its controls".into()); return Err("Pomodoro session changed; refresh its controls".into());
} }
let now = ferese_ipc::activities::now_ms(); let now = ferese_ipc::activities::now_ms();
if !matches!(request.action, PomodoroAction::Stop | PomodoroAction::Reset) && self.advance_pomodoro(now) { if !matches!(request.action, PomodoroAction::Stop | PomodoroAction::Reset)
&& self.advance_pomodoro(now)
{
self.changed(); self.changed();
} }
match request.action { match request.action {
@@ -85,8 +87,8 @@ impl Engine {
} }
let session = Session::start(self.pomodoro_settings.clone(), now)?; let session = Session::start(self.pomodoro_settings.clone(), now)?;
// Verify the long-lived host, never the short-lived CLI, as owner. // Verify the long-lived host, never the short-lived CLI, as owner.
let (owner, _pidfd, _) = let (owner, _pidfd, _) = crate::ipc::activity_owner::inspect(std::process::id())
crate::ipc::activity_owner::inspect(std::process::id()).map_err(|error| error.to_string())?; .map_err(|error| error.to_string())?;
let id = Id { let id = Id {
owner, owner,
local: "pomodoro".into(), local: "pomodoro".into(),
@@ -104,7 +106,10 @@ impl Engine {
self.pomodoro = None; self.pomodoro = None;
} }
action => { action => {
let timer = self.pomodoro.as_mut().ok_or("No Pomodoro session; start one first")?; let timer = self
.pomodoro
.as_mut()
.ok_or("No Pomodoro session; start one first")?;
match action { match action {
PomodoroAction::Pause => timer.session.pause(now)?, PomodoroAction::Pause => timer.session.pause(now)?,
PomodoroAction::Resume => timer.session.resume(now)?, PomodoroAction::Resume => timer.session.resume(now)?,
@@ -182,14 +187,24 @@ impl Alerts {
let connection = zbus::blocking::connection::Builder::session()? let connection = zbus::blocking::connection::Builder::session()?
.method_timeout(Duration::from_secs(2)) .method_timeout(Duration::from_secs(2))
.build()?; .build()?;
let mut hints = std::collections::HashMap::<&str, zbus::zvariant::Value<'_>>::new(); let mut hints =
std::collections::HashMap::<&str, zbus::zvariant::Value<'_>>::new();
hints.insert("suppress-sound", zbus::zvariant::Value::Bool(true)); hints.insert("suppress-sound", zbus::zvariant::Value::Bool(true));
let reply = connection.call_method( let reply = connection.call_method(
Some("org.freedesktop.Notifications"), Some("org.freedesktop.Notifications"),
"/org/freedesktop/Notifications", "/org/freedesktop/Notifications",
Some("org.freedesktop.Notifications"), Some("org.freedesktop.Notifications"),
"Notify", "Notify",
&("Ferese", 0_u32, "", title, body, Vec::<String>::new(), hints, -1_i32), &(
"Ferese",
0_u32,
"",
title,
body,
Vec::<String>::new(),
hints,
-1_i32,
),
)?; )?;
let _: u32 = reply.body().deserialize()?; let _: u32 = reply.body().deserialize()?;
Ok(()) Ok(())
@@ -218,8 +233,7 @@ mod tests {
} }
#[test] #[test]
fn completion_uses_the_standard_notification_service_without_sound() { fn completion_uses_the_standard_notification_service_without_sound() {
const TEST: &str = const TEST: &str = "activities::pomodoro::tests::completion_uses_the_standard_notification_service_without_sound";
"activities::pomodoro::tests::completion_uses_the_standard_notification_service_without_sound";
if !crate::startup_tests::private_runtime(TEST) { if !crate::startup_tests::private_runtime(TEST) {
return; return;
} }
@@ -296,7 +310,10 @@ mod tests {
assert_eq!(first.id.owner.pid, std::process::id()); assert_eq!(first.id.owner.pid, std::process::id());
assert!(control(&mut engine, PomodoroAction::Start).is_err()); assert!(control(&mut engine, PomodoroAction::Start).is_err());
engine.remove_connection(10); engine.remove_connection(10);
engine.owner_exit(Owner { pid: 999_999, birth: 0 }); engine.owner_exit(Owner {
pid: 999_999,
birth: 0,
});
assert_eq!(engine.snapshot().pomodoro.as_ref().unwrap().id, first.id); assert_eq!(engine.snapshot().pomodoro.as_ref().unwrap().id, first.id);
assert_eq!(engine.snapshot().activities.len(), 1); assert_eq!(engine.snapshot().activities.len(), 1);
control(&mut engine, PomodoroAction::Reset).unwrap(); control(&mut engine, PomodoroAction::Reset).unwrap();
@@ -355,7 +372,10 @@ mod tests {
engine.pomodoro_remove_output("test", None); engine.pomodoro_remove_output("test", None);
assert!(engine.pomodoro_attach_output("new")); assert!(engine.pomodoro_attach_output("new"));
engine.changed(); engine.changed();
assert_eq!(engine.snapshot().activities[0].data.output.as_deref(), Some("new")); assert_eq!(
engine.snapshot().activities[0].data.output.as_deref(),
Some("new")
);
} }
#[test] #[test]
fn disabled_alerts_and_schema_errors_have_no_side_effects() { fn disabled_alerts_and_schema_errors_have_no_side_effects() {
+19 -4
View File
@@ -26,7 +26,10 @@ impl Publisher {
.as_ref() .as_ref()
.is_some_and(|(snapshot, _)| Arc::ptr_eq(snapshot, &job.snapshot)) .is_some_and(|(snapshot, _)| Arc::ptr_eq(snapshot, &job.snapshot))
{ {
cached = Some((job.snapshot.clone(), serde_json::json!(job.snapshot.as_ref()))); cached = Some((
job.snapshot.clone(),
serde_json::json!(job.snapshot.as_ref()),
));
} }
let value = &cached.as_ref().unwrap().1; let value = &cached.as_ref().unwrap().1;
// Lock acquisition revokes queued/in-flight serialization. // Lock acquisition revokes queued/in-flight serialization.
@@ -54,7 +57,12 @@ impl Publisher {
} }
} }
} }
fn deliver(epoch: u64, current: &RwLock<u64>, recipients: Vec<Recipient>, value: &serde_json::Value) { fn deliver(
epoch: u64,
current: &RwLock<u64>,
recipients: Vec<Recipient>,
value: &serde_json::Value,
) {
let current = current.read().expect("activity publication epoch"); let current = current.read().expect("activity publication epoch");
if epoch != *current { if epoch != *current {
locked(recipients); locked(recipients);
@@ -116,9 +124,16 @@ mod tests {
let publisher = Publisher::new().unwrap(); let publisher = Publisher::new().unwrap();
let (slow, _slow_receive) = mpsc::sync_channel(1); let (slow, _slow_receive) = mpsc::sync_channel(1);
slow.send(Response::success(1, serde_json::json!({}))).unwrap(); slow.send(Response::success(1, serde_json::json!({})))
.unwrap();
let (fast, fast_receive) = mpsc::sync_channel(1); let (fast, fast_receive) = mpsc::sync_channel(1);
publisher.send(Arc::new(Snapshot::default()), vec![(2, slow), (3, fast)]); publisher.send(Arc::new(Snapshot::default()), vec![(2, slow), (3, fast)]);
assert_eq!(fast_receive.recv_timeout(Duration::from_secs(1)).unwrap().id, 3); assert_eq!(
fast_receive
.recv_timeout(Duration::from_secs(1))
.unwrap()
.id,
3
);
} }
} }
+31 -10
View File
@@ -27,13 +27,20 @@ impl Watcher {
std::thread::Builder::new() std::thread::Builder::new()
.name("ferese-activity-processes".into()) .name("ferese-activity-processes".into())
.spawn(move || { .spawn(move || {
let mut observed = let mut observed = BTreeMap::<
BTreeMap::<Owner, (Arc<std::os::fd::OwnedFd>, ferese_config::activities::Matched)>::new(); Owner,
(
Arc<std::os::fd::OwnedFd>,
ferese_config::activities::Matched,
),
>::new();
while !worker_stop.load(Ordering::Acquire) { while !worker_stop.load(Ordering::Acquire) {
let rules = rules.read().expect("activity rules").clone(); let rules = rules.read().expect("activity rules").clone();
let dead: Vec<_> = observed let dead: Vec<_> = observed
.iter() .iter()
.filter_map(|(owner, (fd, _))| crate::ipc::activity_owner::exited(fd).then_some(*owner)) .filter_map(|(owner, (fd, _))| {
crate::ipc::activity_owner::exited(fd).then_some(*owner)
})
.collect(); .collect();
for owner in dead { for owner in dead {
observed.remove(&owner); observed.remove(&owner);
@@ -54,21 +61,30 @@ impl Watcher {
if worker_stop.load(Ordering::Acquire) || observed.len() >= 128 { if worker_stop.load(Ordering::Acquire) || observed.len() >= 128 {
break; break;
} }
let Some(pid) = process.file_name().to_str().and_then(|name| name.parse::<u32>().ok()) let Some(pid) = process
.file_name()
.to_str()
.and_then(|name| name.parse::<u32>().ok())
else { else {
continue; continue;
}; };
let Ok((owner, fd, parents)) = crate::ipc::activity_owner::inspect(pid) else { let Ok((owner, fd, parents)) = crate::ipc::activity_owner::inspect(pid)
else {
continue; continue;
}; };
if observed.contains_key(&owner) || parents.iter().any(|parent| parent == "feresectl") { if observed.contains_key(&owner)
|| parents.iter().any(|parent| parent == "feresectl")
{
continue; continue;
} }
let Ok(file) = std::fs::File::open(process.path().join("cmdline")) else { let Ok(file) = std::fs::File::open(process.path().join("cmdline"))
else {
continue; continue;
}; };
let mut bytes = Vec::new(); let mut bytes = Vec::new();
if file.take(4_097).read_to_end(&mut bytes).is_err() || bytes.len() > 4_096 { if file.take(4_097).read_to_end(&mut bytes).is_err()
|| bytes.len() > 4_096
{
continue; continue;
} }
let Ok(command) = String::from_utf8(bytes) else { let Ok(command) = String::from_utf8(bytes) else {
@@ -79,14 +95,19 @@ impl Watcher {
continue; continue;
}; };
let cwd = cwd.to_string_lossy(); let cwd = cwd.to_string_lossy();
let Some(matched) = rules.matcher.matches(&command, &cwd, &parents) else { let Some(matched) = rules.matcher.matches(&command, &cwd, &parents)
else {
continue; continue;
}; };
let fd = Arc::new(fd); let fd = Arc::new(fd);
// Keep the inspected process identity alive through dispatch. // Keep the inspected process identity alive through dispatch.
// No raw command, cwd or environment crosses the dispatcher. // No raw command, cwd or environment crosses the dispatcher.
if sender if sender
.try_send(IpcEvent::ActivityDetected(owner, Box::new(matched.clone()), fd.clone())) .try_send(IpcEvent::ActivityDetected(
owner,
Box::new(matched.clone()),
fd.clone(),
))
.is_ok() .is_ok()
{ {
observed.insert(owner, (fd, matched)); observed.insert(owner, (fd, matched));
File diff suppressed because it is too large. Load diff
+78 -35
View File
@@ -17,31 +17,32 @@ pub(crate) fn touch_cache(state: &mut Ferese) {
if state.capture_last_used.replace(Instant::now()).is_some() { if state.capture_last_used.replace(Instant::now()).is_some() {
return; return;
} }
if let Err(error) = state if let Err(error) =
.loop_handle state
.insert_source(Timer::from_duration(CACHE_IDLE), |_, _, state| { .loop_handle
let Some(last_used) = state.capture_last_used else { .insert_source(Timer::from_duration(CACHE_IDLE), |_, _, state| {
return TimeoutAction::Drop; let Some(last_used) = state.capture_last_used else {
}; return TimeoutAction::Drop;
let idle = last_used.elapsed(); };
if idle < CACHE_IDLE { let idle = last_used.elapsed();
return TimeoutAction::ToDuration(CACHE_IDLE - idle); if idle < CACHE_IDLE {
} return TimeoutAction::ToDuration(CACHE_IDLE - idle);
// A stream can pause or fail without another repaint. Release its }
// offscreen targets and filtered-scene caches from the event loop. // A stream can pause or fail without another repaint. Release its
state.capture_last_used = None; // offscreen targets and filtered-scene caches from the event loop.
state.capture_render = Default::default(); state.capture_last_used = None;
state.nested_capture_texture = None; state.capture_render = Default::default();
if let Some(backend) = state.direct_backend.as_mut() { state.nested_capture_texture = None;
for device in backend.devices.values_mut() { if let Some(backend) = state.direct_backend.as_mut() {
for output in device.outputs.values_mut() { for device in backend.devices.values_mut() {
output.capture_texture = None; for output in device.outputs.values_mut() {
output.mirror_capture_texture = None; output.capture_texture = None;
output.mirror_capture_texture = None;
}
} }
} }
} TimeoutAction::Drop
TimeoutAction::Drop })
})
{ {
state.capture_last_used = None; state.capture_last_used = None;
tracing::warn!(%error, "could not schedule capture cache cleanup"); tracing::warn!(%error, "could not schedule capture cache cleanup");
@@ -85,9 +86,18 @@ pub(super) fn capture_mirror(
} }
let elements = capture_output_elements(state, renderer, source, include_cursor, scene); let elements = capture_output_elements(state, renderer, source, include_cursor, scene);
// Never sample mirror_texture: it contains the unfiltered display. // Never sample mirror_texture: it contains the unfiltered display.
let canvas = output.mirror_canvas.as_ref().expect("mirror display composed"); let canvas = output
.mirror_canvas
.as_ref()
.expect("mirror display composed");
let Some(()) = capture_request(state, &output.output, include_cursor, || { let Some(()) = capture_request(state, &output.output, include_cursor, || {
super::mirror::compose(renderer, &mut output.mirror_capture_texture, canvas, source, &elements) super::mirror::compose(
renderer,
&mut output.mirror_capture_texture,
canvas,
source,
&elements,
)
}) else { }) else {
continue; continue;
}; };
@@ -98,7 +108,12 @@ pub(super) fn capture_mirror(
Default::default(), Default::default(),
); );
let Some(target) = capture_request(state, &output.output, include_cursor, || { let Some(target) = capture_request(state, &output.output, include_cursor, || {
render_capture(renderer, &mut output.capture_texture, &output.output, &[element]) render_capture(
renderer,
&mut output.capture_texture,
&output.output,
&[element],
)
}) else { }) else {
continue; continue;
}; };
@@ -168,7 +183,10 @@ mod tests {
#[test] #[test]
#[ignore = "requires an EGL rendering device"] #[ignore = "requires an EGL rendering device"]
fn capture_recomposes_cursor_without_touching_the_display_target() { fn capture_recomposes_cursor_without_touching_the_display_target() {
let device = EGLDevice::enumerate().unwrap().last().expect("an EGL device"); let device = EGLDevice::enumerate()
.unwrap()
.last()
.expect("an EGL device");
let display = unsafe { EGLDisplay::new(device).unwrap() }; let display = unsafe { EGLDisplay::new(device).unwrap() };
let context = EGLContext::new(&display).unwrap(); let context = EGLContext::new(&display).unwrap();
let mut renderer = unsafe { GlesRenderer::new(context).unwrap() }; let mut renderer = unsafe { GlesRenderer::new(context).unwrap() };
@@ -223,18 +241,32 @@ mod tests {
] { ] {
let target = render_capture(&mut renderer, &mut capture, &output, &elements).unwrap(); let target = render_capture(&mut renderer, &mut capture, &output, &elements).unwrap();
let mapping = renderer let mapping = renderer
.copy_framebuffer(&target, Rectangle::from_size((32, 24).into()), Fourcc::Abgr8888) .copy_framebuffer(
&target,
Rectangle::from_size((32, 24).into()),
Fourcc::Abgr8888,
)
.unwrap(); .unwrap();
let pixels = renderer.map_texture(&mapping).unwrap(); let pixels = renderer.map_texture(&mapping).unwrap();
assert_eq!(&pixels[..4], &expected); assert_eq!(&pixels[..4], &expected);
assert_eq!(&pixels[(12 * 32 + 16) * 4..(12 * 32 + 16) * 4 + 4], &[255, 0, 0, 255]); assert_eq!(
&pixels[(12 * 32 + 16) * 4..(12 * 32 + 16) * 4 + 4],
&[255, 0, 0, 255]
);
drop(target); drop(target);
let target = renderer.bind(displayed.as_mut().unwrap()).unwrap(); let target = renderer.bind(displayed.as_mut().unwrap()).unwrap();
let mapping = renderer let mapping = renderer
.copy_framebuffer(&target, Rectangle::from_size((32, 24).into()), Fourcc::Abgr8888) .copy_framebuffer(
&target,
Rectangle::from_size((32, 24).into()),
Fourcc::Abgr8888,
)
.unwrap(); .unwrap();
assert_eq!(&renderer.map_texture(&mapping).unwrap()[..4], &[255, 0, 0, 255]); assert_eq!(
&renderer.map_texture(&mapping).unwrap()[..4],
&[255, 0, 0, 255]
);
} }
output.change_current_state( output.change_current_state(
@@ -246,7 +278,9 @@ mod tests {
None, None,
None, None,
); );
let _ = render_capture::<SolidColorRenderElement>(&mut renderer, &mut capture, &output, &[]).unwrap(); let _ =
render_capture::<SolidColorRenderElement>(&mut renderer, &mut capture, &output, &[])
.unwrap();
assert_eq!(capture.as_ref().unwrap().size(), (48, 16).into()); assert_eq!(capture.as_ref().unwrap().size(), (48, 16).into());
} }
} }
@@ -318,7 +352,10 @@ mod failure_tests {
Ok::<(), ()>(()) Ok::<(), ()>(())
}; };
after_queue(queued, || { after_queue(queued, || {
assert!(submitted.get(), "capture must run after the display queue succeeds"); assert!(
submitted.get(),
"capture must run after the display queue succeeds"
);
assert!( assert!(
capture_request(&mut state, &output, false, || Err::<(), _>( capture_request(&mut state, &output, false, || Err::<(), _>(
"injected allocation failure" "injected allocation failure"
@@ -328,14 +365,20 @@ mod failure_tests {
}) })
.unwrap(); .unwrap();
assert!(submitted.get(), "capture failure must not undo queued display work"); assert!(
submitted.get(),
"capture failure must not undo queued display work"
);
assert_eq!(state.pending_screencopies.len(), 1); assert_eq!(state.pending_screencopies.len(), 1);
assert_eq!(state.pending_screencopies[0].output, other_output); assert_eq!(state.pending_screencopies[0].output, other_output);
let outcome = receiver.try_recv().unwrap(); let outcome = receiver.try_recv().unwrap();
assert_eq!(outcome.request, 41); assert_eq!(outcome.request, 41);
assert_eq!(outcome.part, 0); assert_eq!(outcome.part, 0);
assert!(outcome.result.is_err()); assert!(outcome.result.is_err());
assert!(receiver.try_recv().is_err(), "request should fail exactly once"); assert!(
receiver.try_recv().is_err(),
"request should fail exactly once"
);
assert!( assert!(
other_receiver.try_recv().is_err(), other_receiver.try_recv().is_err(),
"other output's request must stay pending" "other output's request must stay pending"
@@ -130,17 +130,31 @@ mod tests {
#[test] #[test]
fn every_connector_order_finds_a_complete_matching() { fn every_connector_order_finds_a_complete_matching() {
let graph = [&[1, 2][..], &[1, 3][..], &[1, 3][..]]; let graph = [&[1, 2][..], &[1, 3][..], &[1, 3][..]];
for order in [[0, 1, 2], [0, 2, 1], [1, 0, 2], [1, 2, 0], [2, 0, 1], [2, 1, 0]] { for order in [
[0, 1, 2],
[0, 2, 1],
[1, 0, 2],
[1, 2, 0],
[2, 0, 1],
[2, 1, 0],
] {
let requests = order.map(|index| candidates(None, None, graph[index])); let requests = order.map(|index| candidates(None, None, graph[index]));
let result = assign(&requests); let result = assign(&requests);
assert_eq!(result.iter().flatten().count(), 3, "order {order:?}"); assert_eq!(result.iter().flatten().count(), 3, "order {order:?}");
assert_eq!(assign(&requests), result, "deterministic for the same inventory"); assert_eq!(
assign(&requests),
result,
"deterministic for the same inventory"
);
} }
} }
#[test] #[test]
fn equal_preferences_use_connector_order() { fn equal_preferences_use_connector_order() {
let result = assign(&[candidates(Some(1), None, &[1, 2]), candidates(Some(1), None, &[1, 2])]); let result = assign(&[
candidates(Some(1), None, &[1, 2]),
candidates(Some(1), None, &[1, 2]),
]);
assert_eq!(result, vec![Some(crtc(1)), Some(crtc(2))]); assert_eq!(result, vec![Some(crtc(1)), Some(crtc(2))]);
} }
@@ -159,18 +173,29 @@ mod tests {
#[test] #[test]
fn maximum_cardinality_and_preference_objective_match_exhaustive_oracle() { fn maximum_cardinality_and_preference_objective_match_exhaustive_oracle() {
fn score(requests: &[Candidates], assignment: &[Option<crtc::Handle>]) -> (usize, usize, usize) { fn score(
requests: &[Candidates],
assignment: &[Option<crtc::Handle>],
) -> (usize, usize, usize) {
let retained = [true, false].map(|applied| { let retained = [true, false].map(|applied| {
requests requests
.iter() .iter()
.zip(assignment) .zip(assignment)
.filter(|(request, actual)| { .filter(|(request, actual)| {
let preferred = if applied { request.applied } else { request.kernel }; let preferred = if applied {
request.applied
} else {
request.kernel
};
preferred.is_some() && preferred == **actual preferred.is_some() && preferred == **actual
}) })
.count() .count()
}); });
(assignment.iter().flatten().count(), retained[0], retained[1]) (
assignment.iter().flatten().count(),
retained[0],
retained[1],
)
} }
fn enumerate( fn enumerate(
@@ -204,7 +229,8 @@ mod tests {
.filter(|crtc| graph & (1 << (index * 3 + crtc - 1)) != 0) .filter(|crtc| graph & (1 << (index * 3 + crtc - 1)) != 0)
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let applied = ((seed + index) % 4 != 0).then_some((seed + index) % 4); let applied = ((seed + index) % 4 != 0).then_some((seed + index) % 4);
let kernel = ((seed * 3 + index) % 4 != 0).then_some((seed * 3 + index) % 4); let kernel =
((seed * 3 + index) % 4 != 0).then_some((seed * 3 + index) % 4);
candidates(applied, kernel, &compatible) candidates(applied, kernel, &compatible)
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
@@ -213,13 +239,14 @@ mod tests {
let actual = assign(&requests); let actual = assign(&requests);
let used = actual.iter().flatten().collect::<HashSet<_>>(); let used = actual.iter().flatten().collect::<HashSet<_>>();
assert_eq!(used.len(), actual.iter().flatten().count()); assert_eq!(used.len(), actual.iter().flatten().count());
assert!( assert!(requests.iter().zip(&actual).all(|(request, crtc)| {
requests crtc.is_none_or(|crtc| request.compatible.contains(&crtc))
.iter() }));
.zip(&actual) assert_eq!(
.all(|(request, crtc)| crtc.is_none_or(|crtc| request.compatible.contains(&crtc))) score(&requests, &actual),
best,
"graph {graph}, preferences {seed}"
); );
assert_eq!(score(&requests, &actual), best, "graph {graph}, preferences {seed}");
} }
} }
} }
@@ -250,10 +277,16 @@ mod tests {
// The internal panel appears first in the DRM connector inventory. // The internal panel appears first in the DRM connector inventory.
let internal = candidates(None, None, &[151, 270]); let internal = candidates(None, None, &[151, 270]);
let external = candidates(Some(151), Some(151), &[151, 270]); let external = candidates(Some(151), Some(151), &[151, 270]);
assert_eq!(assign(&[internal, external]), vec![Some(crtc(270)), Some(crtc(151))]); assert_eq!(
assign(&[internal, external]),
vec![Some(crtc(270)), Some(crtc(151))]
);
let internal = candidates(None, None, &[151, 270]); let internal = candidates(None, None, &[151, 270]);
let external = candidates(Some(151), Some(151), &[151, 270]); let external = candidates(Some(151), Some(151), &[151, 270]);
assert_eq!(assign(&[external, internal]), vec![Some(crtc(151)), Some(crtc(270))]); assert_eq!(
assign(&[external, internal]),
vec![Some(crtc(151)), Some(crtc(270))]
);
} }
#[test] #[test]
@@ -304,7 +337,10 @@ mod tests {
#[test] #[test]
fn restricted_connector_gets_its_only_free_crtc() { fn restricted_connector_gets_its_only_free_crtc() {
assert_eq!( assert_eq!(
assign(&[candidates(None, None, &[151, 270]), candidates(None, None, &[151])]), assign(&[
candidates(None, None, &[151, 270]),
candidates(None, None, &[151])
]),
vec![Some(crtc(270)), Some(crtc(151))] vec![Some(crtc(270)), Some(crtc(151))]
); );
} }
@@ -312,7 +348,10 @@ mod tests {
#[test] #[test]
fn exhausted_crtcs_do_not_displace_surviving_output_or_assign_twice() { fn exhausted_crtcs_do_not_displace_surviving_output_or_assign_twice() {
assert_eq!( assert_eq!(
assign(&[candidates(None, None, &[151]), candidates(Some(151), Some(151), &[151])]), assign(&[
candidates(None, None, &[151]),
candidates(Some(151), Some(151), &[151])
]),
vec![None, Some(crtc(151))] vec![None, Some(crtc(151))]
); );
assert!(assign(&[]).is_empty()); assert!(assign(&[]).is_empty());
+20 -5
View File
@@ -54,7 +54,10 @@ pub(super) struct Diagnostics {
impl Diagnostics { impl Diagnostics {
pub fn from_environment(output: String) -> Self { pub fn from_environment(output: String) -> Self {
Self::new(output, std::env::var_os("FERESE_TRACE_PERFORMANCE").is_some()) Self::new(
output,
std::env::var_os("FERESE_TRACE_PERFORMANCE").is_some(),
)
} }
fn new(output: String, enabled: bool) -> Self { fn new(output: String, enabled: bool) -> Self {
@@ -191,9 +194,18 @@ mod tests {
#[test] #[test]
fn primary_classification_does_not_infer_scanout_from_zero_damage() { fn primary_classification_does_not_infer_scanout_from_zero_damage() {
assert_eq!(PrimaryOutcome::classify(true, false), PrimaryOutcome::DirectScanout); assert_eq!(
assert_eq!(PrimaryOutcome::classify(false, true), PrimaryOutcome::Composed); PrimaryOutcome::classify(true, false),
assert_eq!(PrimaryOutcome::classify(false, false), PrimaryOutcome::ReusedSwapchain); PrimaryOutcome::DirectScanout
);
assert_eq!(
PrimaryOutcome::classify(false, true),
PrimaryOutcome::Composed
);
assert_eq!(
PrimaryOutcome::classify(false, false),
PrimaryOutcome::ReusedSwapchain
);
} }
#[test] #[test]
@@ -224,7 +236,10 @@ mod tests {
let mut d = Diagnostics::new("test".into(), true); let mut d = Diagnostics::new("test".into(), true);
let queued = Err::<(), _>("queue failed"); let queued = Err::<(), _>("queue failed");
d.queue_result(&queued, PrimaryOutcome::DirectScanout, true, true); d.queue_result(&queued, PrimaryOutcome::DirectScanout, true, true);
assert!(super::super::capture::after_queue(queued, || panic!("failed queues must not capture")).is_err()); assert!(
super::super::capture::after_queue(queued, || panic!("failed queues must not capture"))
.is_err()
);
assert_eq!(d.counters.submissions, 0); assert_eq!(d.counters.submissions, 0);
assert_eq!(d.counters.scanout_submissions, 0); assert_eq!(d.counters.scanout_submissions, 0);
assert_eq!(d.counters.cursor_plane_submissions, 0); assert_eq!(d.counters.cursor_plane_submissions, 0);
+79 -23
View File
@@ -72,17 +72,27 @@ impl OutputFeedback {
// Restrict the preference to the observed primary format/modifier: // Restrict the preference to the observed primary format/modifier:
// without ALLOW_PRIMARY_PLANE_SCANOUT_ANY Smithay compares the full // without ALLOW_PRIMARY_PLANE_SCANOUT_ANY Smithay compares the full
// format with the swapchain. Renderer-only formats remain in fallback. // format with the swapchain. Renderer-only formats remain in fallback.
match DmabufFeedbackBuilder::new(self.render_device, self.render_formats.iter().copied()) match DmabufFeedbackBuilder::new(
.add_preference_tranche(self.scanout_device, TrancheFlags::Scanout, [format], 4..=6) self.render_device,
.build() self.render_formats.iter().copied(),
)
.add_preference_tranche(self.scanout_device, TrancheFlags::Scanout, [format], 4..=6)
.build()
{ {
Ok(feedback) => self.preferred = Some(feedback), Ok(feedback) => self.preferred = Some(feedback),
Err(error) => tracing::warn!(%error, "output DMA-BUF feedback unavailable; retaining render fallback"), Err(error) => {
tracing::warn!(%error, "output DMA-BUF feedback unavailable; retaining render fallback")
}
} }
} }
} }
pub fn observed_frame(&mut self, format: Option<Format>, states: &RenderElementStates, candidates: HashSet<Id>) { pub fn observed_frame(
&mut self,
format: Option<Format>,
states: &RenderElementStates,
candidates: HashSet<Id>,
) {
self.observe_format(format); self.observe_format(format);
self.visible = states self.visible = states
.states .states
@@ -203,13 +213,17 @@ pub(crate) fn refresh(state: &mut Ferese) {
.filter(|id| { .filter(|id| {
state state
.current_window_presentation(*id) .current_window_presentation(*id)
.is_some_and(|presentation| presentation.alpha() == 1.0 && presentation.dim == 0.0) .is_some_and(|presentation| {
presentation.alpha() == 1.0 && presentation.dim == 0.0
})
}) })
.and_then(|id| state.windows.window(id)) .and_then(|id| state.windows.window(id))
.and_then(|window| window.toplevel()) .and_then(|window| window.toplevel())
.filter(|toplevel| { .filter(|toplevel| {
with_renderer_surface_state(toplevel.wl_surface(), |state| state.buffer().is_some()) with_renderer_surface_state(toplevel.wl_surface(), |state| {
.unwrap_or(false) state.buffer().is_some()
})
.unwrap_or(false)
}) })
.map(|toplevel| Id::from(toplevel.wl_surface())) .map(|toplevel| Id::from(toplevel.wl_surface()))
}) })
@@ -251,8 +265,10 @@ mod tests {
let feedback = DmabufFeedbackBuilder::new(10, [format(Fourcc::Argb8888, Modifier::Linear)]) let feedback = DmabufFeedbackBuilder::new(10, [format(Fourcc::Argb8888, Modifier::Linear)])
.build() .build()
.unwrap(); .unwrap();
let global = state.create_global_with_default_feedback::<Ferese>(&display.handle(), &feedback); let global =
let other = state.create_global_with_default_feedback::<Ferese>(&display.handle(), &feedback); state.create_global_with_default_feedback::<Ferese>(&display.handle(), &feedback);
let other =
state.create_global_with_default_feedback::<Ferese>(&display.handle(), &feedback);
(display, global, other) (display, global, other)
} }
fn visible(output: &mut OutputFeedback, id: &Id, f: Format) { fn visible(output: &mut OutputFeedback, id: &Id, f: Format) {
@@ -273,11 +289,31 @@ mod tests {
let linear = format(Fourcc::Argb8888, Modifier::Linear); let linear = format(Fourcc::Argb8888, Modifier::Linear);
let implicit = format(Fourcc::Argb8888, Modifier::Invalid); let implicit = format(Fourcc::Argb8888, Modifier::Invalid);
let opaque = format(Fourcc::Xrgb8888, Modifier::Linear); let opaque = format(Fourcc::Xrgb8888, Modifier::Linear);
assert!(compatible(&formats(&[linear, implicit]), &formats(&[linear]), linear)); assert!(compatible(
assert!(!compatible(&formats(&[linear]), &formats(&[implicit]), linear)); &formats(&[linear, implicit]),
assert!(!compatible(&formats(&[linear]), &formats(&[opaque]), linear)); &formats(&[linear]),
assert!(!compatible(&formats(&[linear]), &formats(&[implicit]), implicit)); linear
assert!(!compatible(&formats(&[opaque]), &formats(&[linear]), linear)); ));
assert!(!compatible(
&formats(&[linear]),
&formats(&[implicit]),
linear
));
assert!(!compatible(
&formats(&[linear]),
&formats(&[opaque]),
linear
));
assert!(!compatible(
&formats(&[linear]),
&formats(&[implicit]),
implicit
));
assert!(!compatible(
&formats(&[opaque]),
&formats(&[linear]),
linear
));
} }
#[test] #[test]
@@ -307,7 +343,11 @@ mod tests {
let mut b = output(&[f], &[f]); let mut b = output(&[f], &[f]);
visible(&mut a, &id, f); visible(&mut a, &id, f);
visible(&mut b, &id, f); visible(&mut b, &id, f);
fn view(global: DmabufGlobal, feedback: &OutputFeedback, eligible: Option<Id>) -> OutputView<'_> { fn view(
global: DmabufGlobal,
feedback: &OutputFeedback,
eligible: Option<Id>,
) -> OutputView<'_> {
OutputView { OutputView {
global, global,
feedback, feedback,
@@ -320,7 +360,10 @@ mod tests {
select( select(
global, global,
&id, &id,
[view(global, &a, Some(id.clone())), view(global, &b, Some(id.clone()))] [
view(global, &a, Some(id.clone())),
view(global, &b, Some(id.clone()))
]
) )
.is_none() .is_none()
); );
@@ -331,7 +374,10 @@ mod tests {
select( select(
global, global,
&id, &id,
[view(global, &a, Some(id.clone())), view(global, &b, Some(id.clone()))] [
view(global, &a, Some(id.clone())),
view(global, &b, Some(id.clone()))
]
) )
.is_some() .is_some()
); );
@@ -347,7 +393,12 @@ mod tests {
let mut o = output(&[f], &[f]); let mut o = output(&[f], &[f]);
visible(&mut o, &id, f); visible(&mut o, &id, f);
o.invalidate(); o.invalidate();
assert!(o.observed.is_none() && o.preferred.is_none() && o.visible.is_empty() && o.candidates.is_empty()); assert!(
o.observed.is_none()
&& o.preferred.is_none()
&& o.visible.is_empty()
&& o.candidates.is_empty()
);
o.observe_format(None); o.observe_format(None);
assert!(o.preferred.is_none()); assert!(o.preferred.is_none());
visible(&mut o, &id, f); visible(&mut o, &id, f);
@@ -390,9 +441,12 @@ mod tests {
"clipped storage must remain hidden" "clipped storage must remain hidden"
); );
let faded = solid(bounds, 0.8); let faded = solid(bounds, 0.8);
assert!(!candidate_geometry(&faded, bounds, scale.into(), || panic!( assert!(!candidate_geometry(
"faded elements must not probe storage" &faded,
))); bounds,
scale.into(),
|| panic!("faded elements must not probe storage")
));
let mut partial = bounds; let mut partial = bounds;
partial.size.w -= 1; partial.size.w -= 1;
assert!(!candidate_geometry( assert!(!candidate_geometry(
@@ -474,7 +528,9 @@ mod tests {
let f = format(Fourcc::Argb8888, Modifier::Linear); let f = format(Fourcc::Argb8888, Modifier::Linear);
let id = Id::new(); let id = Id::new();
let mut o = output(&[f], &[f]); let mut o = output(&[f], &[f]);
let failed = super::super::capture::after_queue(Err::<(), _>("queue failed"), || visible(&mut o, &id, f)); let failed = super::super::capture::after_queue(Err::<(), _>("queue failed"), || {
visible(&mut o, &id, f)
});
assert!(failed.is_err()); assert!(failed.is_err());
assert!(o.visible.is_empty() && o.preferred.is_none()); assert!(o.visible.is_empty() && o.preferred.is_none());
super::super::capture::after_queue(Ok::<(), ()>(()), || visible(&mut o, &id, f)).unwrap(); super::super::capture::after_queue(Ok::<(), ()>(()), || visible(&mut o, &id, f)).unwrap();
+76 -24
View File
@@ -24,8 +24,14 @@ struct Transition {
// A single owner determines both the desired ramp and who loses control on failure. // A single owner determines both the desired ramp and who loses control on failure.
enum Source { enum Source {
Original, Original,
Client { control: ZwlrGammaControlV1, ramp: Ramp }, Client {
NightLight { temperature: u16, ramp: Ramp }, control: ZwlrGammaControlV1,
ramp: Ramp,
},
NightLight {
temperature: u16,
ramp: Ramp,
},
} }
#[derive(Default)] #[derive(Default)]
@@ -136,7 +142,8 @@ impl Gamma {
fn configure(&mut self, settings: Settings, duration: Duration, now: Instant) { fn configure(&mut self, settings: Settings, duration: Duration, now: Instant) {
if settings.enabled { if settings.enabled {
if matches!(self.source, Source::NightLight { temperature, .. } if temperature == settings.temperature) { if matches!(self.source, Source::NightLight { temperature, .. } if temperature == settings.temperature)
{
return; return;
} }
self.replace_source(Source::NightLight { self.replace_source(Source::NightLight {
@@ -152,11 +159,14 @@ impl Gamma {
Source::Original => &self.original, Source::Original => &self.original,
Source::NightLight { ramp, .. } | Source::Client { ramp, .. } => ramp, Source::NightLight { ramp, .. } | Source::Client { ramp, .. } => ramp,
}; };
self.transition = (!duration.is_zero() && !self.suspended && self.current != *target).then(|| Transition { self.transition =
from: self.current.clone(), (!duration.is_zero() && !self.suspended && self.current != *target).then(|| {
started: now, Transition {
duration, from: self.current.clone(),
}); started: now,
duration,
}
});
} }
fn owns(&self, owner: &ZwlrGammaControlV1) -> bool { fn owns(&self, owner: &ZwlrGammaControlV1) -> bool {
@@ -181,8 +191,10 @@ impl Gamma {
}; };
let animated; let animated;
let ramp = if let Some(transition) = &self.transition { let ramp = if let Some(transition) = &self.transition {
let progress = let progress = now
now.saturating_duration_since(transition.started).as_secs_f64() / transition.duration.as_secs_f64(); .saturating_duration_since(transition.started)
.as_secs_f64()
/ transition.duration.as_secs_f64();
if progress >= 1. { if progress >= 1. {
self.transition = None; self.transition = None;
ramp ramp
@@ -222,15 +234,26 @@ impl Drop for Gamma {
} }
} }
pub(crate) fn acquire(state: &mut Ferese, target: &Output, owner: &ZwlrGammaControlV1) -> Option<usize> { pub(crate) fn acquire(
state: &mut Ferese,
target: &Output,
owner: &ZwlrGammaControlV1,
) -> Option<usize> {
// Reconcile a just-reloaded configuration before assigning ownership. // Reconcile a just-reloaded configuration before assigning ownership.
sync_night_light(state); sync_night_light(state);
if state.night_light.enabled { if state.night_light.enabled {
return None; return None;
} }
let duration = state.animation_duration(TRANSITION_DURATION); let duration = state.animation_duration(TRANSITION_DURATION);
let backend = state.direct_backend.as_mut().filter(|backend| backend.active)?; let backend = state
for device in backend.devices.values_mut().filter(|device| device.drm.is_active()) { .direct_backend
.as_mut()
.filter(|backend| backend.active)?;
for device in backend
.devices
.values_mut()
.filter(|device| device.drm.is_active())
{
for (crtc, output) in &mut device.outputs { for (crtc, output) in &mut device.outputs {
if &output.output != target || output.global.is_none() { if &output.output != target || output.global.is_none() {
continue; continue;
@@ -255,28 +278,41 @@ pub(crate) fn sync_night_light(state: &mut Ferese) {
if !state.gamma_dirty { if !state.gamma_dirty {
return; return;
} }
if !state.direct_backend.as_ref().is_some_and(|backend| backend.active) { if !state
.direct_backend
.as_ref()
.is_some_and(|backend| backend.active)
{
return; return;
} }
state.gamma_dirty = false; state.gamma_dirty = false;
if advance_night_light(state, true) && !state.gamma_timer_pending { if advance_night_light(state, true) && !state.gamma_timer_pending {
let result = state let result = state.loop_handle.insert_source(
.loop_handle Timer::from_duration(TRANSITION_INTERVAL),
.insert_source(Timer::from_duration(TRANSITION_INTERVAL), |_, _, state| { |_, _, state| {
if advance_night_light(state, false) { if advance_night_light(state, false) {
TimeoutAction::ToDuration(TRANSITION_INTERVAL) TimeoutAction::ToDuration(TRANSITION_INTERVAL)
} else { } else {
state.gamma_timer_pending = false; state.gamma_timer_pending = false;
TimeoutAction::Drop TimeoutAction::Drop
} }
}); },
);
match result { match result {
Ok(_) => state.gamma_timer_pending = true, Ok(_) => state.gamma_timer_pending = true,
Err(error) => { Err(error) => {
tracing::warn!(%error, "could not schedule night-light transition"); tracing::warn!(%error, "could not schedule night-light transition");
if let Some(backend) = &mut state.direct_backend { if let Some(backend) = &mut state.direct_backend {
for device in backend.devices.values_mut().filter(|device| device.drm.is_active()) { for device in backend
for output in device.outputs.values_mut().filter(|output| !output.power.needs_wake()) { .devices
.values_mut()
.filter(|device| device.drm.is_active())
{
for output in device
.outputs
.values_mut()
.filter(|output| !output.power.needs_wake())
{
if let Some(gamma) = output.gamma.controller_mut() { if let Some(gamma) = output.gamma.controller_mut() {
gamma.transition = None; gamma.transition = None;
gamma.resume(Instant::now()); gamma.resume(Instant::now());
@@ -292,11 +328,19 @@ pub(crate) fn sync_night_light(state: &mut Ferese) {
fn advance_night_light(state: &mut Ferese, force: bool) -> bool { fn advance_night_light(state: &mut Ferese, force: bool) -> bool {
let duration = state.animation_duration(TRANSITION_DURATION); let duration = state.animation_duration(TRANSITION_DURATION);
let now = Instant::now(); let now = Instant::now();
let Some(backend) = state.direct_backend.as_mut().filter(|backend| backend.active) else { let Some(backend) = state
.direct_backend
.as_mut()
.filter(|backend| backend.active)
else {
return false; return false;
}; };
let mut pending = false; let mut pending = false;
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 (crtc, output) in &mut device.outputs { for (crtc, output) in &mut device.outputs {
let ready = !output.power.needs_wake(); let ready = !output.power.needs_wake();
let gamma = if ready { let gamma = if ready {
@@ -346,7 +390,11 @@ pub(crate) fn night_light_status(state: &Ferese) -> Status {
status status
} }
fn owned<'a>(state: &'a mut Ferese, target: &Output, owner: &ZwlrGammaControlV1) -> Option<(&'a mut Gamma, bool)> { fn owned<'a>(
state: &'a mut Ferese,
target: &Output,
owner: &ZwlrGammaControlV1,
) -> Option<(&'a mut Gamma, bool)> {
let duration = state.animation_duration(TRANSITION_DURATION); let duration = state.animation_duration(TRANSITION_DURATION);
let backend = state.direct_backend.as_mut()?; let backend = state.direct_backend.as_mut()?;
for device in backend.devices.values_mut() { for device in backend.devices.values_mut() {
@@ -362,7 +410,11 @@ fn owned<'a>(state: &'a mut Ferese, target: &Output, owner: &ZwlrGammaControlV1)
None None
} }
pub(crate) fn size(state: &mut Ferese, target: &Output, owner: &ZwlrGammaControlV1) -> Option<usize> { pub(crate) fn size(
state: &mut Ferese,
target: &Output,
owner: &ZwlrGammaControlV1,
) -> Option<usize> {
owned(state, target, owner).map(|(gamma, _)| gamma.original.size()) owned(state, target, owner).map(|(gamma, _)| gamma.original.size())
} }
+11 -6
View File
@@ -182,9 +182,13 @@ impl Reader {
"block", "block",
), ),
) )
.and_then(|reply| reply.body().deserialize::<zbus::zvariant::OwnedFd>()); .and_then(|reply| {
reply.body().deserialize::<zbus::zvariant::OwnedFd>()
});
match inhibitor { match inhibitor {
Ok(descriptor) => worker_lease.lock().unwrap().accept(generation, descriptor), Ok(descriptor) => {
worker_lease.lock().unwrap().accept(generation, descriptor)
}
Err(error) => tracing::warn!(%error, "could not own logind lid policy"), Err(error) => tracing::warn!(%error, "could not own logind lid policy"),
} }
continue; continue;
@@ -302,16 +306,17 @@ fn request(state: &mut Ferese, reactivate: bool, wake: bool) {
backend.lid_reader.lease.lock().unwrap().suspend = false; backend.lid_reader.lease.lock().unwrap().suspend = false;
let probe = backend.lid.begin(reactivate, wake); let probe = backend.lid.begin(reactivate, wake);
let sent = backend.lid_reader.requests.send(probe).is_ok(); let sent = backend.lid_reader.requests.send(probe).is_ok();
let timer = state let timer = state.loop_handle.insert_source(
.loop_handle Timer::from_duration(Duration::from_secs(3)),
.insert_source(Timer::from_duration(Duration::from_secs(3)), move |_, _, state| { move |_, _, state| {
if let Some(backend) = state.direct_backend.as_mut() { if let Some(backend) = state.direct_backend.as_mut() {
backend.lid_reader.deadline = None; backend.lid_reader.deadline = None;
} }
tracing::warn!("lid refresh deadline elapsed; retaining the latest observed state"); tracing::warn!("lid refresh deadline elapsed; retaining the latest observed state");
complete_refresh(state, probe, None); complete_refresh(state, probe, None);
TimeoutAction::Drop TimeoutAction::Drop
}); },
);
if sent && let Ok(token) = timer { if sent && let Ok(token) = timer {
state.direct_backend.as_mut().unwrap().lid_reader.deadline = Some(token); state.direct_backend.as_mut().unwrap().lid_reader.deadline = Some(token);
+42 -19
View File
@@ -6,7 +6,8 @@ use smithay::utils::{Buffer, Physical, Rectangle};
/// Fit the complete source into the transformed target; black letterbox, never crop. /// Fit the complete source into the transformed target; black letterbox, never crop.
fn fit(source: (i32, i32), target: (i32, i32)) -> Rectangle<i32, Physical> { fn fit(source: (i32, i32), target: (i32, i32)) -> Rectangle<i32, Physical> {
let scale = (target.0 as f64 / source.0.max(1) as f64).min(target.1 as f64 / source.1.max(1) as f64); let scale =
(target.0 as f64 / source.0.max(1) as f64).min(target.1 as f64 / source.1.max(1) as f64);
let width = (source.0 as f64 * scale).round().max(1.0) as i32; let width = (source.0 as f64 * scale).round().max(1.0) as i32;
let height = (source.1 as f64 * scale).round().max(1.0) as i32; let height = (source.1 as f64 * scale).round().max(1.0) as i32;
Rectangle::new( Rectangle::new(
@@ -21,7 +22,10 @@ pub(super) fn elements(
source: &Output, source: &Output,
frame: &crate::state::FrameScene, frame: &crate::state::FrameScene,
target: &mut DirectOutput, target: &mut DirectOutput,
) -> Result<Vec<crate::render::AnimatedWindowRenderElement>, smithay::backend::renderer::gles::GlesError> { ) -> Result<
Vec<crate::render::AnimatedWindowRenderElement>,
smithay::backend::renderer::gles::GlesError,
> {
let elements = sampled_output_elements(state, renderer, source, true, frame); let elements = sampled_output_elements(state, renderer, source, true, frame);
// Target renderer owns this texture, including when source and target use different GPUs. // Target renderer owns this texture, including when source and target use different GPUs.
let canvas = target.mirror_canvas.get_or_insert_with(|| { let canvas = target.mirror_canvas.get_or_insert_with(|| {
@@ -36,7 +40,13 @@ pub(super) fn elements(
}, },
) )
}); });
compose(renderer, &mut target.mirror_texture, canvas, source, &elements)?; compose(
renderer,
&mut target.mirror_texture,
canvas,
source,
&elements,
)?;
let texture = target let texture = target
.mirror_texture .mirror_texture
.as_ref() .as_ref()
@@ -44,7 +54,13 @@ pub(super) fn elements(
.clone(); .clone();
target.mirror_commit.increment(); target.mirror_commit.increment();
Ok(vec![ Ok(vec![
fitted_texture(texture, &target.output, target.mirror_id.clone(), target.mirror_commit).into(), fitted_texture(
texture,
&target.output,
target.mirror_id.clone(),
target.mirror_commit,
)
.into(),
]) ])
} }
@@ -141,19 +157,20 @@ mod tests {
None, None,
); );
let canvas = output("canvas"); let canvas = output("canvas");
let colors: Vec<crate::render::AnimatedWindowRenderElement> = [(0, [1., 0., 0., 1.]), (32, [0., 0., 1., 1.])] let colors: Vec<crate::render::AnimatedWindowRenderElement> =
.into_iter() [(0, [1., 0., 0., 1.]), (32, [0., 0., 1., 1.])]
.map(|(y, color)| { .into_iter()
SolidColorRenderElement::new( .map(|(y, color)| {
Id::new(), SolidColorRenderElement::new(
Rectangle::new((0, y).into(), (32, 32).into()), Id::new(),
CommitCounter::default(), Rectangle::new((0, y).into(), (32, 32).into()),
Color32F::from(color), CommitCounter::default(),
Kind::Unspecified, Color32F::from(color),
) Kind::Unspecified,
.into() )
}) .into()
.collect(); })
.collect();
let mut texture = None; let mut texture = None;
compose(&mut renderer, &mut texture, &canvas, &source, &colors).unwrap(); compose(&mut renderer, &mut texture, &canvas, &source, &colors).unwrap();
assert_eq!(texture.as_ref().unwrap().size(), (32, 64).into()); assert_eq!(texture.as_ref().unwrap().size(), (32, 64).into());
@@ -175,9 +192,15 @@ mod tests {
CommitCounter::default(), CommitCounter::default(),
); );
let mut target_texture = None; let mut target_texture = None;
let framebuffer = capture::render_capture(&mut renderer, &mut target_texture, &target, &[element]).unwrap(); let framebuffer =
capture::render_capture(&mut renderer, &mut target_texture, &target, &[element])
.unwrap();
let mapping = renderer let mapping = renderer
.copy_framebuffer(&framebuffer, Rectangle::from_size((96, 64).into()), Fourcc::Abgr8888) .copy_framebuffer(
&framebuffer,
Rectangle::from_size((96, 64).into()),
Fourcc::Abgr8888,
)
.unwrap(); .unwrap();
let pixels = renderer.map_texture(&mapping).unwrap(); let pixels = renderer.map_texture(&mapping).unwrap();
let red = pixels let red = pixels
+47 -30
View File
@@ -8,7 +8,8 @@ pub(super) fn inject(mode: &str) -> bool {
use std::sync::LazyLock; use std::sync::LazyLock;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
static MODE: LazyLock<Option<String>> = LazyLock::new(|| std::env::var("FERESE_TEST_DRM_FAILURE").ok()); static MODE: LazyLock<Option<String>> =
LazyLock::new(|| std::env::var("FERESE_TEST_DRM_FAILURE").ok());
static USED: AtomicBool = AtomicBool::new(false); static USED: AtomicBool = AtomicBool::new(false);
MODE.as_deref() == Some(mode) && !USED.swap(true, Ordering::AcqRel) MODE.as_deref() == Some(mode) && !USED.swap(true, Ordering::AcqRel)
} }
@@ -57,31 +58,38 @@ pub(super) fn arm_completion(
} }
let identity = output.output.clone(); let identity = output.output.clone();
match handle.insert_source(Timer::from_duration(COMPLETION_TIMEOUT), move |_, _, state| { match handle.insert_source(
let Some(output) = state Timer::from_duration(COMPLETION_TIMEOUT),
.direct_backend move |_, _, state| {
.as_mut() let Some(output) = state
.and_then(|backend| backend.devices.get_mut(&node)) .direct_backend
.and_then(|device| device.outputs.get_mut(&crtc)) .as_mut()
.filter(|output| output.output == identity) .and_then(|backend| backend.devices.get_mut(&node))
else { .and_then(|device| device.outputs.get_mut(&crtc))
return TimeoutAction::Drop; .filter(|output| output.output == identity)
}; else {
return TimeoutAction::Drop;
};
if let Some(remaining) = output.completion.remaining(Instant::now()) { if let Some(remaining) = output.completion.remaining(Instant::now()) {
if !remaining.is_zero() { if !remaining.is_zero() {
return TimeoutAction::ToDuration(remaining); return TimeoutAction::ToDuration(remaining);
}
output.completion_timer = None;
tracing::error!(
?node,
?crtc,
"DRM frame completion timed out; reconciling device"
);
reconcile_device(state, node);
} else {
output.completion_timer = None;
} }
output.completion_timer = None; TimeoutAction::Drop
tracing::error!(?node, ?crtc, "DRM frame completion timed out; reconciling device"); },
reconcile_device(state, node); ) {
} else {
output.completion_timer = None;
}
TimeoutAction::Drop
}) {
Ok(token) => { Ok(token) => {
output.completion_timer = Some(token); output.completion_timer = Some(token);
true true
@@ -106,10 +114,12 @@ pub(super) fn reconcile_device(state: &mut Ferese, node: DrmNode) {
// sessions retain the invalidation for fresh enumeration on activation. // sessions retain the invalidation for fresh enumeration on activation.
let active = backend.active; let active = backend.active;
if active { if active {
match state.loop_handle.insert_source(Timer::immediate(), |_, _, state| { match state
reconcile_outputs(state, false); .loop_handle
TimeoutAction::Drop .insert_source(Timer::immediate(), |_, _, state| {
}) { reconcile_outputs(state, false);
TimeoutAction::Drop
}) {
Ok(_) => (), Ok(_) => (),
Err(error) => tracing::error!(%error, "could not schedule DRM device recovery"), Err(error) => tracing::error!(%error, "could not schedule DRM device recovery"),
} }
@@ -125,10 +135,14 @@ mod tests {
let now = Instant::now(); let now = Instant::now();
let mut completion = Completion::default(); let mut completion = Completion::default();
completion.queued(now); completion.queued(now);
let (feedback, retired, error) = retirement::<(), _>(Err("injected KMS retirement failure")); let (feedback, retired, error) =
retirement::<(), _>(Err("injected KMS retirement failure"));
assert_eq!(feedback, None); assert_eq!(feedback, None);
assert!(!retired); assert!(!retired);
assert!(error.is_some(), "the production handler must reconcile the device"); assert!(
error.is_some(),
"the production handler must reconcile the device"
);
assert_eq!(completion.remaining(now), Some(COMPLETION_TIMEOUT)); assert_eq!(completion.remaining(now), Some(COMPLETION_TIMEOUT));
assert_eq!(retirement::<(), &str>(Ok(None)), (None, false, None)); assert_eq!(retirement::<(), &str>(Ok(None)), (None, false, None));
assert_eq!(retirement::<(), &str>(Ok(Some(()))), (Some(()), true, None)); assert_eq!(retirement::<(), &str>(Ok(Some(()))), (Some(()), true, None));
@@ -148,7 +162,10 @@ mod tests {
Some(Duration::from_millis(1)) Some(Duration::from_millis(1))
); );
// A timeout observes uncertainty; it never frees the current buffer. // A timeout observes uncertainty; it never frees the current buffer.
assert_eq!(completion.remaining(now + COMPLETION_TIMEOUT), Some(Duration::ZERO)); assert_eq!(
completion.remaining(now + COMPLETION_TIMEOUT),
Some(Duration::ZERO)
);
assert_eq!( assert_eq!(
completion.remaining(now + COMPLETION_TIMEOUT + Duration::from_secs(1)), completion.remaining(now + COMPLETION_TIMEOUT + Duration::from_secs(1)),
Some(Duration::ZERO) Some(Duration::ZERO)
+69 -16
View File
@@ -13,7 +13,13 @@ impl AnimationFallback {
self.orphaned_since.is_some() self.orphaned_since.is_some()
} }
pub fn deadline(&mut self, now: Instant, interval: Duration, active: bool, chained: bool) -> Option<Instant> { pub fn deadline(
&mut self,
now: Instant,
interval: Duration,
active: bool,
chained: bool,
) -> Option<Instant> {
if !active || chained { if !active || chained {
self.orphaned_since = None; self.orphaned_since = None;
return None; return None;
@@ -55,12 +61,21 @@ impl SchedulingMetrics {
} }
} }
pub fn redraw(&mut self, source: &'static Location<'static>, global: bool, requested: usize, connected: usize) { pub fn redraw(
&mut self,
source: &'static Location<'static>,
global: bool,
requested: usize,
connected: usize,
) {
if !self.enabled { if !self.enabled {
return; return;
} }
let count = self.redraws.entry((source.file(), source.line(), global)).or_default(); let count = self
.redraws
.entry((source.file(), source.line(), global))
.or_default();
count.calls += 1; count.calls += 1;
count.requested_outputs += requested as u64; count.requested_outputs += requested as u64;
count.connected_outputs += connected as u64; count.connected_outputs += connected as u64;
@@ -119,8 +134,11 @@ pub(super) fn cursor_on_output(state: &crate::Ferese, output: &smithay::output::
.map(|attributes| attributes.lock().unwrap().hotspot) .map(|attributes| attributes.lock().unwrap().hotspot)
.unwrap_or_default() .unwrap_or_default()
}); });
smithay::desktop::utils::bbox_from_surface_tree(surface, (location - hotspot.to_f64()).to_i32_round()) smithay::desktop::utils::bbox_from_surface_tree(
.to_f64() surface,
(location - hotspot.to_f64()).to_i32_round(),
)
.to_f64()
} }
CursorImageStatus::Named(_) => { CursorImageStatus::Named(_) => {
@@ -135,7 +153,11 @@ pub(super) fn cursor_on_output(state: &crate::Ferese, output: &smithay::output::
// One logical pixel covers physical rounding at fractional scales. // One logical pixel covers physical rounding at fractional scales.
let bounds = Rectangle::<f64, Logical>::new( let bounds = Rectangle::<f64, Logical>::new(
(f64::from(bounds.loc.x) - 1.0, f64::from(bounds.loc.y) - 1.0).into(), (f64::from(bounds.loc.x) - 1.0, f64::from(bounds.loc.y) - 1.0).into(),
(f64::from(bounds.size.w) + 2.0, f64::from(bounds.size.h) + 2.0).into(), (
f64::from(bounds.size.w) + 2.0,
f64::from(bounds.size.h) + 2.0,
)
.into(),
); );
rect.overlaps(bounds) rect.overlaps(bounds)
} }
@@ -152,7 +174,10 @@ mod tests {
let mut idle_fast = AnimationFallback::default(); let mut idle_fast = AnimationFallback::default();
let mut moving_slow = AnimationFallback::default(); let mut moving_slow = AnimationFallback::default();
assert_eq!(idle_fast.deadline(now, fast, false, false), None); assert_eq!(idle_fast.deadline(now, fast, false, false), None);
assert_eq!(moving_slow.deadline(now, slow, true, false), Some(now + slow * 2)); assert_eq!(
moving_slow.deadline(now, slow, true, false),
Some(now + slow * 2)
);
assert_eq!( assert_eq!(
moving_slow.deadline(now + slow, slow, true, false), moving_slow.deadline(now + slow, slow, true, false),
Some(now + slow * 2) Some(now + slow * 2)
@@ -166,7 +191,10 @@ mod tests {
let interval = Duration::from_millis(16); let interval = Duration::from_millis(16);
let mut output = AnimationFallback::default(); let mut output = AnimationFallback::default();
output.deadline(now, interval, true, false); output.deadline(now, interval, true, false);
assert_eq!(output.deadline(now + interval, interval, false, false), None); assert_eq!(
output.deadline(now + interval, interval, false, false),
None
);
let reconnected = now + Duration::from_secs(1); let reconnected = now + Duration::from_secs(1);
assert_eq!( assert_eq!(
output.deadline(reconnected, interval, true, false), output.deadline(reconnected, interval, true, false),
@@ -192,7 +220,10 @@ mod tests {
last_tick_us = now_us; last_tick_us = now_us;
} }
assert_eq!(policy.deadline(start, Duration::from_micros(16_667), true, true), None); assert_eq!(
policy.deadline(start, Duration::from_micros(16_667), true, true),
None
);
} }
assert_eq!(old_wakes, 60); assert_eq!(old_wakes, 60);
@@ -210,9 +241,17 @@ mod tests {
assert_eq!(fallback.deadline(start, interval, true, true), None); assert_eq!(fallback.deadline(start, interval, true, true), None);
let plan = scheduler.begin().unwrap(); let plan = scheduler.begin().unwrap();
assert_eq!(plan, scheduled); assert_eq!(plan, scheduled);
scheduler.rendered(plan, Duration::from_millis(2), Duration::from_millis(2), true); scheduler.rendered(
plan,
Duration::from_millis(2),
Duration::from_millis(2),
true,
);
assert_eq!(scheduler.plan(interval), None); assert_eq!(scheduler.plan(interval), None);
assert_eq!(fallback.deadline(start + interval, interval, true, true), None); assert_eq!(
fallback.deadline(start + interval, interval, true, true),
None
);
scheduler.presented(interval); scheduler.presented(interval);
assert_eq!( assert_eq!(
fallback.deadline(start + interval, interval, true, false), fallback.deadline(start + interval, interval, true, false),
@@ -220,7 +259,10 @@ mod tests {
); );
scheduler.request(interval); scheduler.request(interval);
assert!(scheduler.plan(interval).is_some()); assert!(scheduler.plan(interval).is_some());
assert_eq!(fallback.deadline(start + interval, interval, true, true), None); assert_eq!(
fallback.deadline(start + interval, interval, true, true),
None
);
} }
#[test] #[test]
@@ -237,13 +279,17 @@ mod tests {
assert!(scheduler.has_pending_request()); assert!(scheduler.has_pending_request());
let deadline = fallback.deadline(start, interval, true, false).unwrap(); let deadline = fallback.deadline(start, interval, true, false).unwrap();
let animation_active = false; let animation_active = false;
let needs_final_frame = animation_active || (fallback.waiting_for_frame() && scheduler.has_pending_request()); let needs_final_frame =
animation_active || (fallback.waiting_for_frame() && scheduler.has_pending_request());
assert_eq!( assert_eq!(
fallback.deadline(deadline, interval, needs_final_frame, false), fallback.deadline(deadline, interval, needs_final_frame, false),
Some(deadline) Some(deadline)
); );
// A successful retry installs the normal timer and cancels fallback. // A successful retry installs the normal timer and cancels fallback.
assert_eq!(fallback.deadline(deadline, interval, needs_final_frame, true), None); assert_eq!(
fallback.deadline(deadline, interval, needs_final_frame, true),
None
);
scheduler.begin().unwrap(); scheduler.begin().unwrap();
assert!(!scheduler.has_pending_request()); assert!(!scheduler.has_pending_request());
} }
@@ -296,7 +342,11 @@ mod tests {
refresh, refresh,
}), }),
None, None,
Some(smithay::output::Scale::Fractional(if index == 0 { 2.0 } else { 1.0 })), Some(smithay::output::Scale::Fractional(if index == 0 {
2.0
} else {
1.0
})),
Some(((index as i32) * 1920, 0).into()), Some(((index as i32) * 1920, 0).into()),
); );
state.space.map_output(&output, ((index as i32) * 1920, 0)); state.space.map_output(&output, ((index as i32) * 1920, 0));
@@ -342,7 +392,10 @@ mod tests {
}, },
); );
assert!(cursor_on_output(&state, &outputs[0])); assert!(cursor_on_output(&state, &outputs[0]));
assert!(cursor_on_output(&state, &outputs[1]), "cursor image straddles the seam"); assert!(
cursor_on_output(&state, &outputs[1]),
"cursor image straddles the seam"
);
if let Some(cursor) = state.named_cursor_frame() { if let Some(cursor) = state.named_cursor_frame() {
let x = 1920.0 - f64::from(cursor.size.w) / f64::from(cursor.buffer_scale) * 0.75 let x = 1920.0 - f64::from(cursor.size.w) / f64::from(cursor.buffer_scale) * 0.75
+ f64::from(cursor.hotspot.x) / f64::from(cursor.buffer_scale); + f64::from(cursor.hotspot.x) / f64::from(cursor.buffer_scale);
+9 -2
View File
@@ -138,8 +138,15 @@ fn dispatch_frame(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle, identit
arm_animation_timer(state); arm_animation_timer(state);
} }
pub(super) fn record_frame_schedule(output: &mut DirectOutput, plan: FramePlan, cost: Duration, submitted: bool) { pub(super) fn record_frame_schedule(
output.scheduler.rendered(plan, monotonic_now(), cost, submitted); output: &mut DirectOutput,
plan: FramePlan,
cost: Duration,
submitted: bool,
) {
output
.scheduler
.rendered(plan, monotonic_now(), cost, submitted);
if std::env::var_os("FERESE_TRACE_PERFORMANCE").is_some() { if std::env::var_os("FERESE_TRACE_PERFORMANCE").is_some() {
let started = monotonic_now().saturating_sub(cost); let started = monotonic_now().saturating_sub(cost);
tracing::debug!(target: "ferese::render", output = %output.output.name(), tracing::debug!(target: "ferese::render", output = %output.output.name(),
@@ -8,8 +8,14 @@ pub(super) struct SessionDevice<S: Session> {
impl<S: Session> SessionDevice<S> { impl<S: Session> SessionDevice<S> {
pub fn open(mut session: S, path: &Path) -> Result<Self, S::Error> { pub fn open(mut session: S, path: &Path) -> Result<Self, S::Error> {
let fd = session.open(path, OFlags::RDWR | OFlags::CLOEXEC | OFlags::NOCTTY | OFlags::NONBLOCK)?; let fd = session.open(
Ok(Self { session, fd: Some(fd) }) path,
OFlags::RDWR | OFlags::CLOEXEC | OFlags::NOCTTY | OFlags::NONBLOCK,
)?;
Ok(Self {
session,
fd: Some(fd),
})
} }
pub fn device_fd(&self) -> io::Result<DeviceFd> { pub fn device_fd(&self) -> io::Result<DeviceFd> {
@@ -20,7 +26,10 @@ impl<S: Session> SessionDevice<S> {
impl<S: Session> Drop for SessionDevice<S> { impl<S: Session> Drop for SessionDevice<S> {
fn drop(&mut self) { fn drop(&mut self) {
if let Err(error) = self.session.close(self.fd.take().unwrap()) { if let Err(error) = self.session.close(self.fd.take().unwrap()) {
tracing::warn!(?error, "could not release DRM device through the session manager"); tracing::warn!(
?error,
"could not release DRM device through the session manager"
);
} }
} }
} }
+150 -35
View File
@@ -31,11 +31,20 @@ impl<Node> Default for DeviceState<Node> {
pub(super) fn device_identity(path: &Path) -> Option<PathBuf> { pub(super) fn device_identity(path: &Path) -> Option<PathBuf> {
// The parent hardware identity survives cardN renumbering after unplug. // The parent hardware identity survives cardN renumbering after unplug.
std::fs::canonicalize(Path::new("/sys/class/drm").join(path.file_name()?).join("device")).ok() std::fs::canonicalize(
Path::new("/sys/class/drm")
.join(path.file_name()?)
.join("device"),
)
.ok()
} }
impl<Node> DeviceState<Node> { impl<Node> DeviceState<Node> {
fn current_path<'a>(&self, original: &Path, observed: &'a [(PathBuf, Option<PathBuf>)]) -> Option<&'a Path> { fn current_path<'a>(
&self,
original: &Path,
observed: &'a [(PathBuf, Option<PathBuf>)],
) -> Option<&'a Path> {
observed observed
.iter() .iter()
.find(|(path, identity)| match &self.identity { .find(|(path, identity)| match &self.identity {
@@ -48,7 +57,8 @@ impl<Node> DeviceState<Node> {
pub fn failed(&mut self, now: Instant) { pub fn failed(&mut self, now: Instant) {
self.availability = Availability::Unavailable; self.availability = Availability::Unavailable;
self.failures = self.failures.saturating_add(1); self.failures = self.failures.saturating_add(1);
self.retry_at = Some(now + Duration::from_secs(1u64 << self.failures.saturating_sub(1).min(5))); self.retry_at =
Some(now + Duration::from_secs(1u64 << self.failures.saturating_sub(1).min(5)));
} }
pub fn ready(&mut self, node: Node) { pub fn ready(&mut self, node: Node) {
@@ -128,7 +138,8 @@ impl<Node: Copy + Eq + std::hash::Hash> Topology<Node> {
) -> HashSet<Node> { ) -> HashSet<Node> {
let mut retired = HashSet::new(); let mut retired = HashSet::new();
for (key, slot) in &mut self.devices { for (key, slot) in &mut self.devices {
if let Availability::Ready(previous) | Availability::Degraded(previous) = slot.availability if let Availability::Ready(previous) | Availability::Degraded(previous) =
slot.availability
&& slot.current_path(key, observed).and_then(&node_for_path) != Some(previous) && slot.current_path(key, observed).and_then(&node_for_path) != Some(previous)
{ {
retired.insert(previous); retired.insert(previous);
@@ -145,7 +156,12 @@ impl<Node: Copy + Eq + std::hash::Hash> Topology<Node> {
pub fn retry_deadline(&self, active: bool) -> Option<Instant> { pub fn retry_deadline(&self, active: bool) -> Option<Instant> {
active active
.then(|| self.devices.values().filter_map(|device| device.retry_at).min()) .then(|| {
self.devices
.values()
.filter_map(|device| device.retry_at)
.min()
})
.flatten() .flatten()
} }
} }
@@ -166,7 +182,10 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) {
let Some(backend) = state.direct_backend.as_mut() else { let Some(backend) = state.direct_backend.as_mut() else {
return; return;
}; };
if !backend.topology.request(backend.active && !backend.lid.pending()) { if !backend
.topology
.request(backend.active && !backend.lid.pending())
{
return; return;
} }
@@ -200,8 +219,11 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) {
.output_profiles .output_profiles
.iter() .iter()
.any(|profile| profile.outputs.iter().any(|output| output.auto_refresh)); .any(|profile| profile.outputs.iter().any(|output| output.auto_refresh));
backend.low_power = backend.low_power = automatic
automatic && super::super::power::low_power(backend.low_power, super::super::power::battery_percent()); && super::super::power::low_power(
backend.low_power,
super::super::power::battery_percent(),
);
if let Ok(observed) = &observed { if let Ok(observed) = &observed {
backend.topology.observe_devices(observed); backend.topology.observe_devices(observed);
@@ -230,7 +252,14 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) {
.and_then(|devices| slot.current_path(&path, devices)); .and_then(|devices| slot.current_path(&path, devices));
let existing = current_path let existing = current_path
.and_then(|path| DrmNode::from_path(path).ok()) .and_then(|path| DrmNode::from_path(path).ok())
.filter(|node| state.direct_backend.as_ref().unwrap().devices.contains_key(node)); .filter(|node| {
state
.direct_backend
.as_ref()
.unwrap()
.devices
.contains_key(node)
});
let previous = match slot.availability { let previous = match slot.availability {
Availability::Ready(node) | Availability::Degraded(node) => Some(node), Availability::Ready(node) | Availability::Degraded(node) => Some(node),
_ => existing, _ => existing,
@@ -269,7 +298,13 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) {
let mut failed_outputs = Vec::new(); let mut failed_outputs = Vec::new();
let activation = if reactivate && existing.is_some() { let activation = if reactivate && existing.is_some() {
let device = state.direct_backend.as_mut().unwrap().devices.get_mut(&node).unwrap(); let device = state
.direct_backend
.as_mut()
.unwrap()
.devices
.get_mut(&node)
.unwrap();
device.drm.activate(true).map(|()| { device.drm.activate(true).map(|()| {
for (crtc, output) in &mut device.outputs { for (crtc, output) in &mut device.outputs {
cancel_output_timers(&state.loop_handle, output); cancel_output_timers(&state.loop_handle, output);
@@ -333,7 +368,12 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) {
ready.push((path, node)); ready.push((path, node));
} }
let known_good = state.direct_backend.as_ref().unwrap().applied_outputs.clone(); let known_good = state
.direct_backend
.as_ref()
.unwrap()
.applied_outputs
.clone();
if let Err(error) = refresh_monitor_policy(state).and_then(|()| { if let Err(error) = refresh_monitor_policy(state).and_then(|()| {
transaction::retire_disconnected(state); transaction::retire_disconnected(state);
validate_desired_outputs(state) validate_desired_outputs(state)
@@ -353,7 +393,10 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) {
.outputs .outputs
.iter() .iter()
.filter(|output| output.settings.enabled && output.key.starts_with(&prefix)); .filter(|output| output.settings.enabled && output.key.starts_with(&prefix));
let logical_sources = desired.clone().filter(|output| output.mirror_source.is_none()).count(); let logical_sources = desired
.clone()
.filter(|output| output.mirror_source.is_none())
.count();
std::cmp::Reverse((logical_sources, desired.count())) std::cmp::Reverse((logical_sources, desired.count()))
}); });
let mut succeeded = Vec::new(); let mut succeeded = Vec::new();
@@ -380,7 +423,13 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) {
break; break;
} }
} }
if state.direct_backend.as_ref().unwrap().output_error.is_none() { if state
.direct_backend
.as_ref()
.unwrap()
.output_error
.is_none()
{
remember_applied_configuration(state); remember_applied_configuration(state);
} }
} }
@@ -390,7 +439,9 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) {
if std::mem::take(&mut state.direct_backend.as_mut().unwrap().topology.layout_dirty) { if std::mem::take(&mut state.direct_backend.as_mut().unwrap().topology.layout_dirty) {
desktop_changes.layout = true; desktop_changes.layout = true;
desktop_changes.shell = true; desktop_changes.shell = true;
desktop_changes.outputs.extend(state.space.outputs().cloned()); desktop_changes
.outputs
.extend(state.space.outputs().cloned());
} }
let backend = state.direct_backend.as_mut().unwrap(); let backend = state.direct_backend.as_mut().unwrap();
backend.topology.reconciling = false; backend.topology.reconciling = false;
@@ -453,7 +504,8 @@ fn publish_runtime_desktop(state: &mut Ferese) -> crate::state::DesktopChanges {
}); });
let mode = OutputMode::from(mode); let mode = OutputMode::from(mode);
let transform = output_transform(settings.transform); let transform = output_transform(settings.transform);
let width = (transform.transform_size(mode.size).w as f64 / settings.scale).ceil() as i32; let width =
(transform.transform_size(mode.size).w as f64 / settings.scale).ceil() as i32;
right_edge = right_edge.max(position[0].saturating_add(width)); right_edge = right_edge.max(position[0].saturating_add(width));
crate::state::DesktopOutput { crate::state::DesktopOutput {
output, output,
@@ -472,7 +524,8 @@ fn publish_runtime_desktop(state: &mut Ferese) -> crate::state::DesktopChanges {
for output in device.outputs.values_mut() { for output in device.outputs.values_mut() {
if output.mirror_source.is_none() { if output.mirror_source.is_none() {
if output.global.is_none() { if output.global.is_none() {
output.global = Some(output.output.create_global::<Ferese>(&state.display_handle)); output.global =
Some(output.output.create_global::<Ferese>(&state.display_handle));
} }
} else { } else {
// Mirrors are physical scanouts, not workspace-owning outputs. // Mirrors are physical scanouts, not workspace-owning outputs.
@@ -483,7 +536,9 @@ fn publish_runtime_desktop(state: &mut Ferese) -> crate::state::DesktopChanges {
None, None,
); );
} }
output.surface.set_output_mode_source((&output.output).into()); output
.surface
.set_output_mode_source((&output.output).into());
} }
} }
changes changes
@@ -542,7 +597,11 @@ mod tests {
topology.devices.get_mut(&a).unwrap().ready(node); topology.devices.get_mut(&a).unwrap().ready(node);
// A policy failure keeps the device open, so it must retain ownership // A policy failure keeps the device open, so it must retain ownership
// of its node until replacement retirement runs. // of its node until replacement retirement runs.
topology.devices.get_mut(&a).unwrap().degraded(node, Instant::now()); topology
.devices
.get_mut(&a)
.unwrap()
.degraded(node, Instant::now());
let observed = [(card.clone(), Some(b.clone()))]; let observed = [(card.clone(), Some(b.clone()))];
topology.observe_devices(&observed); topology.observe_devices(&observed);
assert_eq!( assert_eq!(
@@ -551,9 +610,16 @@ mod tests {
); );
assert_eq!(topology.devices[&a].availability, Availability::Missing); assert_eq!(topology.devices[&a].availability, Availability::Missing);
assert_eq!(topology.devices[&a].current_path(&a, &observed), None); assert_eq!(topology.devices[&a].current_path(&a, &observed), None);
assert_eq!(topology.devices[&b].current_path(&b, &observed), Some(card.as_path())); assert_eq!(
topology.devices[&b].current_path(&b, &observed),
Some(card.as_path())
);
topology.devices.get_mut(&b).unwrap().ready(node); topology.devices.get_mut(&b).unwrap().ready(node);
assert!(topology.retire_changed_devices(&observed, |_| Some(node)).is_empty()); assert!(
topology
.retire_changed_devices(&observed, |_| Some(node))
.is_empty()
);
} }
#[test] #[test]
@@ -567,15 +633,28 @@ mod tests {
let mut topology = Topology::<u64>::default(); let mut topology = Topology::<u64>::default();
topology.observe_devices(&[(card0.clone(), Some(a.clone()))]); topology.observe_devices(&[(card0.clone(), Some(a.clone()))]);
topology.devices.get_mut(&a).unwrap().ready(node0); topology.devices.get_mut(&a).unwrap().ready(node0);
let observed = [(card0.clone(), Some(b.clone())), (card2.clone(), Some(a.clone()))]; let observed = [
(card0.clone(), Some(b.clone())),
(card2.clone(), Some(a.clone())),
];
for _ in 0..2 { for _ in 0..2 {
topology.observe_devices(&observed); topology.observe_devices(&observed);
} }
assert_eq!(topology.devices.len(), 2); assert_eq!(topology.devices.len(), 2);
assert_eq!(topology.devices[&a].current_path(&a, &observed), Some(card2.as_path()));
assert_eq!(topology.devices[&b].current_path(&b, &observed), Some(card0.as_path()));
assert_eq!( assert_eq!(
topology.retire_changed_devices(&observed, |path| Some(if path == card0 { node0 } else { node2 })), topology.devices[&a].current_path(&a, &observed),
Some(card2.as_path())
);
assert_eq!(
topology.devices[&b].current_path(&b, &observed),
Some(card0.as_path())
);
assert_eq!(
topology.retire_changed_devices(&observed, |path| Some(if path == card0 {
node0
} else {
node2
})),
HashSet::from([node0]) HashSet::from([node0])
); );
} }
@@ -590,7 +669,10 @@ mod tests {
topology.devices.get_mut(&card).unwrap().ready(node); topology.devices.get_mut(&card).unwrap().ready(node);
topology.observe_devices(&[(card.clone(), Some(identity.clone()))]); topology.observe_devices(&[(card.clone(), Some(identity.clone()))]);
assert_eq!(topology.devices.len(), 1); assert_eq!(topology.devices.len(), 1);
assert_eq!(topology.devices[&identity].availability, Availability::Ready(node)); assert_eq!(
topology.devices[&identity].availability,
Availability::Ready(node)
);
} }
#[test] #[test]
@@ -608,7 +690,10 @@ mod tests {
topology.retire_changed_devices(&unknown, |_| Some(1)), topology.retire_changed_devices(&unknown, |_| Some(1)),
HashSet::from([1]) HashSet::from([1])
); );
assert_eq!(topology.devices[&identity].availability, Availability::Missing); assert_eq!(
topology.devices[&identity].availability,
Availability::Missing
);
assert_eq!( assert_eq!(
topology.devices[&card].current_path(&card, &unknown), topology.devices[&card].current_path(&card, &unknown),
Some(card.as_path()) Some(card.as_path())
@@ -624,9 +709,17 @@ mod tests {
Some(card.as_path()) Some(card.as_path())
); );
// The reopened node is still valid and can be reused for this identity. // The reopened node is still valid and can be reused for this identity.
assert!(topology.retire_changed_devices(&known, |_| Some(1)).is_empty()); assert!(
topology
.retire_changed_devices(&known, |_| Some(1))
.is_empty()
);
topology.devices.get_mut(&identity).unwrap().ready(1); topology.devices.get_mut(&identity).unwrap().ready(1);
assert!(topology.retire_changed_devices(&known, |_| Some(1)).is_empty()); assert!(
topology
.retire_changed_devices(&known, |_| Some(1))
.is_empty()
);
} }
#[test] #[test]
@@ -636,10 +729,16 @@ mod tests {
let card0 = PathBuf::from("/dev/dri/card0"); let card0 = PathBuf::from("/dev/dri/card0");
let card1 = PathBuf::from("/dev/dri/card1"); let card1 = PathBuf::from("/dev/dri/card1");
let mut topology = Topology::<u64>::default(); let mut topology = Topology::<u64>::default();
topology.observe_devices(&[(card0.clone(), Some(a.clone())), (card1.clone(), Some(b.clone()))]); topology.observe_devices(&[
(card0.clone(), Some(a.clone())),
(card1.clone(), Some(b.clone())),
]);
topology.devices.get_mut(&a).unwrap().ready(0); topology.devices.get_mut(&a).unwrap().ready(0);
topology.devices.get_mut(&b).unwrap().ready(1); topology.devices.get_mut(&b).unwrap().ready(1);
let swapped = [(card0.clone(), Some(b.clone())), (card1.clone(), Some(a.clone()))]; let swapped = [
(card0.clone(), Some(b.clone())),
(card1.clone(), Some(a.clone())),
];
let node_for_path = |path: &Path| Some(u64::from(path == card1)); let node_for_path = |path: &Path| Some(u64::from(path == card1));
topology.observe_devices(&swapped); topology.observe_devices(&swapped);
assert_eq!( assert_eq!(
@@ -650,7 +749,11 @@ mod tests {
assert_eq!(topology.devices[&b].availability, Availability::Missing); assert_eq!(topology.devices[&b].availability, Availability::Missing);
topology.devices.get_mut(&a).unwrap().ready(1); topology.devices.get_mut(&a).unwrap().ready(1);
topology.devices.get_mut(&b).unwrap().ready(0); topology.devices.get_mut(&b).unwrap().ready(0);
assert!(topology.retire_changed_devices(&swapped, node_for_path).is_empty()); assert!(
topology
.retire_changed_devices(&swapped, node_for_path)
.is_empty()
);
} }
#[test] #[test]
@@ -693,12 +796,17 @@ mod tests {
fn unavailable_devices_retry_independently_with_capped_backoff() { fn unavailable_devices_retry_independently_with_capped_backoff() {
let now = Instant::now(); let now = Instant::now();
let mut topology = Topology::<u64>::default(); let mut topology = Topology::<u64>::default();
topology.devices.insert("/dev/dri/card0".into(), DeviceState::default()); topology
.devices
.insert("/dev/dri/card0".into(), DeviceState::default());
let healthy = 1; let healthy = 1;
let mut second = DeviceState::default(); let mut second = DeviceState::default();
second.ready(healthy); second.ready(healthy);
topology.devices.insert("/dev/dri/card1".into(), second); topology.devices.insert("/dev/dri/card1".into(), second);
let device = topology.devices.get_mut(Path::new("/dev/dri/card0")).unwrap(); let device = topology
.devices
.get_mut(Path::new("/dev/dri/card0"))
.unwrap();
device.failed(now); device.failed(now);
assert_eq!(device.retry_at, Some(now + Duration::from_secs(1))); assert_eq!(device.retry_at, Some(now + Duration::from_secs(1)));
for _ in 0..20 { for _ in 0..20 {
@@ -710,8 +818,15 @@ mod tests {
Availability::Ready(healthy) Availability::Ready(healthy)
); );
assert!(topology.retry_deadline(false).is_none()); assert!(topology.retry_deadline(false).is_none());
assert_eq!(topology.retry_deadline(true), Some(now + Duration::from_secs(32))); assert_eq!(
topology.devices.get_mut(Path::new("/dev/dri/card0")).unwrap().missing(); topology.retry_deadline(true),
Some(now + Duration::from_secs(32))
);
topology
.devices
.get_mut(Path::new("/dev/dri/card0"))
.unwrap()
.missing();
assert!(topology.retry_deadline(true).is_none()); assert!(topology.retry_deadline(true).is_none());
} }
} }
+95 -26
View File
@@ -6,7 +6,11 @@ use smithay::backend::allocator::Allocator;
use smithay::backend::drm::exporter::{ExportBuffer, ExportFramebuffer}; use smithay::backend::drm::exporter::{ExportBuffer, ExportFramebuffer};
use smithay::reexports::drm::control::{AtomicCommitFlags, ResourceHandle, atomic::AtomicModeReq}; use smithay::reexports::drm::control::{AtomicCommitFlags, ResourceHandle, atomic::AtomicModeReq};
fn property<D: ControlDevice, H: ResourceHandle>(drm: &D, handle: H, name: &str) -> io::Result<property::Handle> { fn property<D: ControlDevice, H: ResourceHandle>(
drm: &D,
handle: H,
name: &str,
) -> io::Result<property::Handle> {
for (property, _) in drm.get_properties(handle)?.iter() { for (property, _) in drm.get_properties(handle)?.iter() {
if drm.get_property(*property)?.name().to_bytes() == name.as_bytes() { if drm.get_property(*property)?.name().to_bytes() == name.as_bytes() {
return Ok(*property); return Ok(*property);
@@ -35,8 +39,9 @@ fn clear_secondary_planes(
for plane in planes.cursor.iter().chain(&planes.overlay) { for plane in planes.cursor.iter().chain(&planes.overlay) {
let properties = drm.get_properties(plane.handle)?; let properties = drm.get_properties(plane.handle)?;
let owned = properties.iter().any(|(handle, value)| { let owned = properties.iter().any(|(handle, value)| {
drm.get_property(*handle) drm.get_property(*handle).is_ok_and(|info| {
.is_ok_and(|info| info.name().to_bytes() == b"CRTC_ID" && *value == u32::from(crtc) as u64) info.name().to_bytes() == b"CRTC_ID" && *value == u32::from(crtc) as u64
})
}); });
if owned { if owned {
add(drm, request, plane.handle, "FB_ID", 0)?; add(drm, request, plane.handle, "FB_ID", 0)?;
@@ -59,12 +64,14 @@ impl Drop for Blobs {
} }
} }
pub(super) fn validate_device(device: &DirectDevice, scan: &OutputScan) -> Result<(), Box<dyn Error>> { pub(super) fn validate_device(
device: &DirectDevice,
scan: &OutputScan,
) -> Result<(), Box<dyn Error>> {
if scan.selections.iter().all(|selection| { if scan.selections.iter().all(|selection| {
device device.outputs.get(&selection.crtc).is_some_and(|output| {
.outputs output.connector == selection.connector.handle() && output.mode == selection.mode
.get(&selection.crtc) })
.is_some_and(|output| output.connector == selection.connector.handle() && output.mode == selection.mode)
}) && scan.selections.len() == device.outputs.len() }) && scan.selections.len() == device.outputs.len()
{ {
return Ok(()); return Ok(());
@@ -82,7 +89,10 @@ pub(super) fn validate_device(device: &DirectDevice, scan: &OutputScan) -> Resul
}; };
let mut buffers = Vec::new(); let mut buffers = Vec::new();
let mut framebuffers = Vec::new(); let mut framebuffers = Vec::new();
let mut allocator = GbmAllocator::new(device.gbm.clone(), GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT); let mut allocator = GbmAllocator::new(
device.gbm.clone(),
GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT,
);
let exporter = GbmFramebufferExporter::new(device.gbm.clone(), Some(device.render_node).into()); let exporter = GbmFramebufferExporter::new(device.gbm.clone(), Some(device.render_node).into());
let mut used_planes = HashSet::new(); let mut used_planes = HashSet::new();
let desired_crtcs = scan let desired_crtcs = scan
@@ -151,16 +161,33 @@ pub(super) fn validate_device(device: &DirectDevice, scan: &OutputScan) -> Resul
Fourcc::Argb8888 | Fourcc::Abgr8888 | Fourcc::Xrgb8888 | Fourcc::Xbgr8888 Fourcc::Argb8888 | Fourcc::Abgr8888 | Fourcc::Xrgb8888 | Fourcc::Xbgr8888
) )
}) { }) {
if let Ok(buffer) = allocator.create_buffer(width.into(), height.into(), format.code, &[format.modifier]) if let Ok(buffer) = allocator.create_buffer(
&& let Ok(Some(fb)) = exporter.add_framebuffer(drm.device_fd(), ExportBuffer::Allocator(&buffer), false) width.into(),
height.into(),
format.code,
&[format.modifier],
) && let Ok(Some(fb)) =
exporter.add_framebuffer(drm.device_fd(), ExportBuffer::Allocator(&buffer), false)
{ {
allocated = Some((buffer, fb)); allocated = Some((buffer, fb));
break; break;
} }
} }
let (buffer, fb) = allocated.ok_or("unable to allocate a DRM validation buffer")?; let (buffer, fb) = allocated.ok_or("unable to allocate a DRM validation buffer")?;
add(drm, &mut request, primary, "FB_ID", u32::from(*fb.as_ref()) as u64)?; add(
add(drm, &mut request, primary, "CRTC_ID", u32::from(selection.crtc) as u64)?; drm,
&mut request,
primary,
"FB_ID",
u32::from(*fb.as_ref()) as u64,
)?;
add(
drm,
&mut request,
primary,
"CRTC_ID",
u32::from(selection.crtc) as u64,
)?;
for (name, value) in [ for (name, value) in [
("SRC_X", 0), ("SRC_X", 0),
("SRC_Y", 0), ("SRC_Y", 0),
@@ -176,7 +203,10 @@ pub(super) fn validate_device(device: &DirectDevice, scan: &OutputScan) -> Resul
buffers.push(buffer); buffers.push(buffer);
framebuffers.push(fb); framebuffers.push(fb);
} }
drm.atomic_commit(AtomicCommitFlags::TEST_ONLY | AtomicCommitFlags::ALLOW_MODESET, request)?; drm.atomic_commit(
AtomicCommitFlags::TEST_ONLY | AtomicCommitFlags::ALLOW_MODESET,
request,
)?;
Ok(()) Ok(())
} }
@@ -348,13 +378,19 @@ impl crate::output_transaction::HardwareTransaction for DeviceTransaction<'_> {
let previous = self.device.outputs.get_mut(&crtc).unwrap(); let previous = self.device.outputs.get_mut(&crtc).unwrap();
previous.surface = replacement.surface; previous.surface = replacement.surface;
previous.dmabuf_feedback = replacement.dmabuf_feedback; previous.dmabuf_feedback = replacement.dmabuf_feedback;
previous.surface.commit_frame().map_err(|error| error.to_string())?; previous
.surface
.commit_frame()
.map_err(|error| error.to_string())?;
} }
let Some(output) = self.device.outputs.get_mut(&crtc) else { let Some(output) = self.device.outputs.get_mut(&crtc) else {
return Ok(()); return Ok(());
}; };
output.dmabuf_feedback.invalidate(); output.dmabuf_feedback.invalidate();
output.surface.reset_state().map_err(|error| error.to_string())?; output
.surface
.reset_state()
.map_err(|error| error.to_string())?;
let restored = output let restored = output
.surface .surface
.set_connectors(&[output.connector]) .set_connectors(&[output.connector])
@@ -368,9 +404,16 @@ impl crate::output_transaction::HardwareTransaction for DeviceTransaction<'_> {
) )
.map_err(|error| error.to_string()) .map_err(|error| error.to_string())
}) })
.and_then(|()| output.surface.commit_frame().map_err(|error| error.to_string())); .and_then(|()| {
output
.surface
.commit_frame()
.map_err(|error| error.to_string())
});
self.state.session_lock.output_added(&output.output); self.state.session_lock.output_added(&output.output);
self.state.display_presentation.remove_output(&output.output); self.state
.display_presentation
.remove_output(&output.output);
self.state self.state
.desktop_transition .desktop_transition
.as_mut() .as_mut()
@@ -378,7 +421,9 @@ impl crate::output_transaction::HardwareTransaction for DeviceTransaction<'_> {
.redraw .redraw
.insert(output.output.clone()); .insert(output.output.clone());
output.lock_frame_pending = false; output.lock_frame_pending = false;
output.surface.set_output_mode_source((&output.output).into()); output
.surface
.set_output_mode_source((&output.output).into());
cancel_output_timers(&self.state.loop_handle, output); cancel_output_timers(&self.state.loop_handle, output);
output.frame_pending = false; output.frame_pending = false;
restored restored
@@ -447,8 +492,12 @@ pub(super) fn apply_device(
} }
for crtc in &changed { for crtc in &changed {
let selection = &selections[crtc]; let selection = &selections[crtc];
let mut output = staged.remove(crtc).or_else(|| device.outputs.remove(crtc)).unwrap(); let mut output = staged
if output.connector != selection.connector.handle() || output.identity != selection.identity { .remove(crtc)
.or_else(|| device.outputs.remove(crtc))
.unwrap();
if output.connector != selection.connector.handle() || output.identity != selection.identity
{
retire(state, &mut output, node, *crtc); retire(state, &mut output, node, *crtc);
let (handle, _) = create_output( let (handle, _) = create_output(
state, state,
@@ -482,7 +531,9 @@ pub(super) fn apply_device(
state.display_presentation.remove_output(&output.output); state.display_presentation.remove_output(&output.output);
} }
output.lock_frame_pending = false; output.lock_frame_pending = false;
output.surface.set_output_mode_source((&output.output).into()); output
.surface
.set_output_mode_source((&output.output).into());
cancel_output_timers(&state.loop_handle, &mut output); cancel_output_timers(&state.loop_handle, &mut output);
output.frame_pending = false; output.frame_pending = false;
output.primary_commit = None; output.primary_commit = None;
@@ -513,7 +564,11 @@ pub(super) fn update_applied_info(device: &mut DirectDevice) {
for info in &mut device.connected_outputs { for info in &mut device.connected_outputs {
info.enabled = false; info.enabled = false;
info.current_mode = None; info.current_mode = None;
if let Some(output) = device.outputs.values().find(|output| output.identity == info.identity) { if let Some(output) = device
.outputs
.values()
.find(|output| output.identity == info.identity)
{
info.enabled = true; info.enabled = true;
info.current_mode = Some(connected_mode_info(output.mode)); info.current_mode = Some(connected_mode_info(output.mode));
info.scale = output.settings.scale; info.scale = output.settings.scale;
@@ -568,7 +623,12 @@ pub(super) fn recover_last_output(state: &mut Ferese) {
.unwrap() .unwrap()
.devices .devices
.values() .values()
.any(|device| device.outputs.values().any(|output| output.mirror_source.is_none())) .any(|device| {
device
.outputs
.values()
.any(|output| output.mirror_source.is_none())
})
{ {
return; return;
} }
@@ -604,7 +664,11 @@ pub(super) fn recover_last_output(state: &mut Ferese) {
let nodes = backend let nodes = backend
.devices .devices
.iter() .iter()
.filter(|(_, device)| candidate.key.starts_with(&format!("{}/", drm_device_key(&device.drm)))) .filter(|(_, device)| {
candidate
.key
.starts_with(&format!("{}/", drm_device_key(&device.drm)))
})
.map(|(node, _)| *node) .map(|(node, _)| *node)
.collect::<Vec<_>>(); .collect::<Vec<_>>();
if validate_desired_outputs(state).is_ok() { if validate_desired_outputs(state).is_ok() {
@@ -617,7 +681,12 @@ pub(super) fn recover_last_output(state: &mut Ferese) {
.unwrap() .unwrap()
.devices .devices
.values() .values()
.any(|device| device.outputs.values().any(|output| output.mirror_source.is_none())) .any(|device| {
device
.outputs
.values()
.any(|output| output.mirror_source.is_none())
})
{ {
remember_applied_configuration(state); remember_applied_configuration(state);
state.direct_backend.as_mut().unwrap().output_error = original_error; state.direct_backend.as_mut().unwrap().output_error = original_error;
+37 -9
View File
@@ -26,12 +26,16 @@ pub struct LaunchConfig {
impl LaunchConfig { impl LaunchConfig {
pub fn from_environment() -> Result<Self, String> { 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) 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 args = args.into_iter().peekable();
let mut requested_backend = None; let mut requested_backend = None;
let mut client = Vec::new(); let mut client = Vec::new();
@@ -113,7 +117,10 @@ fn parse_backend(value: &OsString) -> Result<Option<BackendKind>, String> {
Some("auto") => Ok(None), Some("auto") => Ok(None),
Some("nested") => Ok(Some(BackendKind::Nested)), Some("nested") => Ok(Some(BackendKind::Nested)),
Some("drm") => Ok(Some(BackendKind::Drm)), 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
)),
} }
} }
@@ -148,7 +155,11 @@ mod tests {
.unwrap(); .unwrap();
assert!(config.supervise_client); assert!(config.supervise_client);
assert_eq!(config.client, [OsString::from("ferese-session-shell")]); assert_eq!(config.client, [OsString::from("ferese-session-shell")]);
assert!(config.client_capabilities.contains(ClientCapabilities::SHELL_CONTROL)); assert!(
config
.client_capabilities
.contains(ClientCapabilities::SHELL_CONTROL)
);
} }
#[test] #[test]
@@ -194,25 +205,42 @@ mod tests {
.unwrap(); .unwrap();
assert_eq!(config.client, [OsString::from("ferese-effects-probe")]); assert_eq!(config.client, [OsString::from("ferese-effects-probe")]);
assert!(config.client_capabilities.contains(ClientCapabilities::EFFECTS)); assert!(
config
.client_capabilities
.contains(ClientCapabilities::EFFECTS)
);
} }
#[test] #[test]
fn explicitly_grants_shell_control_to_the_launched_private_client() { fn explicitly_grants_shell_control_to_the_launched_private_client() {
let config = LaunchConfig::parse( let config = LaunchConfig::parse(
[OsString::from("--grant-shell-control"), OsString::from("ferese-shell")], [
OsString::from("--grant-shell-control"),
OsString::from("ferese-shell"),
],
true, true,
) )
.unwrap(); .unwrap();
assert_eq!(config.client, [OsString::from("ferese-shell")]); assert_eq!(config.client, [OsString::from("ferese-shell")]);
assert!(config.client_capabilities.contains(ClientCapabilities::SHELL_CONTROL)); assert!(
assert!(!config.client_capabilities.contains(ClientCapabilities::EFFECTS)); config
.client_capabilities
.contains(ClientCapabilities::SHELL_CONTROL)
);
assert!(
!config
.client_capabilities
.contains(ClientCapabilities::EFFECTS)
);
} }
#[test] #[test]
fn rejects_an_unknown_backend() { 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")); assert!(error.contains("unknown backend"));
} }
+11 -6
View File
@@ -17,10 +17,10 @@ fn read_battery_percent(root: &Path) -> Option<u8> {
.ok() .ok()
.map(|text| text.trim().to_owned()) .map(|text| text.trim().to_owned())
}; };
if entries if entries.iter().any(|path| {
.iter() read(path, "online").as_deref() == Some("1")
.any(|path| read(path, "online").as_deref() == Some("1") && read(path, "type").as_deref() != Some("Battery")) && read(path, "type").as_deref() != Some("Battery")
{ }) {
return None; return None;
} }
entries.iter().find_map(|path| { entries.iter().find_map(|path| {
@@ -57,7 +57,9 @@ pub(super) fn low_refresh_mode(size: (u16, u16), modes: &[(u16, u16, i32)]) -> O
modes modes
.iter() .iter()
.enumerate() .enumerate()
.filter(|(_, (width, height, refresh))| (*width, *height) == size && (59_000..=61_000).contains(refresh)) .filter(|(_, (width, height, refresh))| {
(*width, *height) == size && (59_000..=61_000).contains(refresh)
})
.min_by_key(|(_, (_, _, refresh))| refresh.abs_diff(60_000)) .min_by_key(|(_, (_, _, refresh))| refresh.abs_diff(60_000))
.map(|(index, _)| index) .map(|(index, _)| index)
} }
@@ -96,7 +98,10 @@ mod tests {
assert_eq!(low_refresh_mode((2880, 1800), &modes), Some(3)); assert_eq!(low_refresh_mode((2880, 1800), &modes), Some(3));
assert_eq!(low_refresh_mode((2880, 1800), &modes[..3]), Some(2)); assert_eq!(low_refresh_mode((2880, 1800), &modes[..3]), Some(2));
assert_eq!(low_refresh_mode((2880, 1800), &modes[..2]), None); assert_eq!(low_refresh_mode((2880, 1800), &modes[..2]), None);
assert_eq!(low_refresh_mode((2880, 1800), &[(2880, 1800, -60_000)]), None); assert_eq!(
low_refresh_mode((2880, 1800), &[(2880, 1800, -60_000)]),
None
);
} }
#[test] #[test]
+196 -79
View File
@@ -26,7 +26,9 @@ use ferese_layout::{ColumnWidth, Direction, GapConfig, ViewportFocusStrategy};
pub use input_motion::InputSettings; pub use input_motion::InputSettings;
use input_motion::*; use input_motion::*;
use outputs::*; use outputs::*;
pub use outputs::{LidPolicy, OutputLayout, OutputModeRequest, OutputProfile, OutputSettings, OutputTransform}; pub use outputs::{
LidPolicy, OutputLayout, OutputModeRequest, OutputProfile, OutputSettings, OutputTransform,
};
use serde::Deserialize; use serde::Deserialize;
use smithay::input::keyboard::{Keycode, keysyms, xkb}; use smithay::input::keyboard::{Keycode, keysyms, xkb};
pub(crate) use tab_indicator::TabIndicatorSettings; pub(crate) use tab_indicator::TabIndicatorSettings;
@@ -128,11 +130,19 @@ impl Config {
ferese_panel::validate(panels).map_err(ConfigError::Binding)?; ferese_panel::validate(panels).map_err(ConfigError::Binding)?;
} }
self.activities.validate().map_err(ConfigError::Binding)?; self.activities.validate().map_err(ConfigError::Binding)?;
self.notifications.validate().map_err(ConfigError::Binding)?; self.notifications
.validate()
.map_err(ConfigError::Binding)?;
self.xwayland.validate().map_err(ConfigError::Binding)?; self.xwayland.validate().map_err(ConfigError::Binding)?;
for daemon in &self.autostart { for daemon in &self.autostart {
if daemon.command.first().is_none_or(|program| program.trim().is_empty()) { if daemon
return Err(ConfigError::Binding("autostart command must contain a program".into())); .command
.first()
.is_none_or(|program| program.trim().is_empty())
{
return Err(ConfigError::Binding(
"autostart command must contain a program".into(),
));
} }
} }
let input_settings = self.input_settings()?; let input_settings = self.input_settings()?;
@@ -451,7 +461,10 @@ mod tests {
let runtime = config.runtime_config().unwrap(); let runtime = config.runtime_config().unwrap();
assert_eq!(runtime.panel_background_opacity, Some(0.8)); assert_eq!(runtime.panel_background_opacity, Some(0.8));
assert!((runtime.theme_settings.shell_opacity - 0.3).abs() < 0.001); assert!((runtime.theme_settings.shell_opacity - 0.3).abs() < 0.001);
let inherited = Config::parse_source("panel main;").unwrap().runtime_config().unwrap(); let inherited = Config::parse_source("panel main;")
.unwrap()
.runtime_config()
.unwrap();
assert!(inherited.panel_background_opacity.is_none()); assert!(inherited.panel_background_opacity.is_none());
} }
@@ -475,7 +488,12 @@ mod tests {
#[test] #[test]
fn validates_shell_composition_before_publishing_config() { fn validates_shell_composition_before_publishing_config() {
let source = r#"panel "main" { end { group "status" { item "network" kind="network"; item "clock" kind="clock"; }; }; }"#; let source = r#"panel "main" { end { group "status" { item "network" kind="network"; item "clock" kind="clock"; }; }; }"#;
assert!(Config::parse_source(source).unwrap().runtime_config().is_ok()); assert!(
Config::parse_source(source)
.unwrap()
.runtime_config()
.is_ok()
);
assert!( assert!(
Config::parse_source(&source.replace("item \"clock\"", "item \"network\"")) Config::parse_source(&source.replace("item \"clock\"", "item \"network\""))
.unwrap() .unwrap()
@@ -498,10 +516,14 @@ mod tests {
"panel main {{ style {}; end {{ group status {{ item clock kind=clock; }}; }}; }}", "panel main {{ style {}; end {{ group status {{ item clock kind=clock; }}; }}; }}",
style.key() style.key()
); );
Config::parse_source(&source).unwrap().runtime_config().unwrap(); Config::parse_source(&source)
.unwrap()
.runtime_config()
.unwrap();
} }
for surface in ["none", "inset", "island"] { for surface in ["none", "inset", "island"] {
let invalid = format!("panel main {{ end {{ group status {{ surface {surface}; }}; }}; }}"); let invalid =
format!("panel main {{ end {{ group status {{ surface {surface}; }}; }}; }}");
assert!(Config::parse_source(&invalid).is_err()); assert!(Config::parse_source(&invalid).is_err());
} }
assert!(Config::parse_source("panel main { group-surface invalid; }").is_err()); assert!(Config::parse_source("panel main { group-surface invalid; }").is_err());
@@ -512,11 +534,18 @@ mod tests {
#[test] #[test]
fn group_padding_is_validated_before_live_publication() { fn group_padding_is_validated_before_live_publication() {
for padding in ["0", "4.5", "12", "32"] { for padding in ["0", "4.5", "12", "32"] {
let source = format!("panel main {{ end {{ group status {{ island-padding {padding}; }}; }}; }}"); let source = format!(
Config::parse_source(&source).unwrap().runtime_config().unwrap(); "panel main {{ end {{ group status {{ island-padding {padding}; }}; }}; }}"
);
Config::parse_source(&source)
.unwrap()
.runtime_config()
.unwrap();
} }
for padding in ["-1", "32.5", "\"small\""] { for padding in ["-1", "32.5", "\"small\""] {
let source = format!("panel main {{ end {{ group status {{ island-padding {padding}; }}; }}; }}"); let source = format!(
"panel main {{ end {{ group status {{ island-padding {padding}; }}; }}; }}"
);
assert!( assert!(
Config::parse_source(&source) Config::parse_source(&source)
.map(|config| config.runtime_config().is_err()) .map(|config| config.runtime_config().is_err())
@@ -529,7 +558,8 @@ mod tests {
fn rejects_removed_physics_keys_before_publication() { fn rejects_removed_physics_keys_before_publication() {
for property in ["spring", "viewport-spring"] { for property in ["spring", "viewport-spring"] {
for coefficient in ["mass", "stiffness", "damping", "damping-ratio"] { for coefficient in ["mass", "stiffness", "damping", "damping-ratio"] {
let source = format!("animations {{ {property} {{ {coefficient} 0; overshoot #true; }}; }}"); let source =
format!("animations {{ {property} {{ {coefficient} 0; overshoot #true; }}; }}");
let error = Config::parse_source(&source).unwrap_err().to_string(); let error = Config::parse_source(&source).unwrap_err().to_string();
assert!(error.contains("unknown field"), "{error}"); assert!(error.contains("unknown field"), "{error}");
} }
@@ -541,10 +571,9 @@ mod tests {
#[test] #[test]
fn display_key_has_a_default_binding_and_can_be_rebound() { fn display_key_has_a_default_binding_and_can_be_rebound() {
let bindings = parse("").runtime_config().unwrap().bindings; let bindings = parse("").runtime_config().unwrap().bindings;
assert!(bindings.iter().any( assert!(bindings.iter().any(|binding| binding.trigger
|binding| binding.trigger == BindingTrigger::Keysym(keysyms::KEY_XF86Display) == BindingTrigger::Keysym(keysyms::KEY_XF86Display)
&& binding.action == BindingAction::ToggleDisplayMode && binding.action == BindingAction::ToggleDisplayMode));
));
let bindings = parse( let bindings = parse(
"binding keys=\"XF86Display\" disabled=#true\nbinding keys=\"Super+P\" action=\"toggle-display-mode\"\n", "binding keys=\"XF86Display\" disabled=#true\nbinding keys=\"Super+P\" action=\"toggle-display-mode\"\n",
) )
@@ -557,9 +586,8 @@ mod tests {
.any(|binding| binding.trigger == BindingTrigger::Keysym(keysyms::KEY_XF86Display)) .any(|binding| binding.trigger == BindingTrigger::Keysym(keysyms::KEY_XF86Display))
); );
assert!( assert!(
bindings bindings.iter().any(|binding| binding.modifiers.logo
.iter() && binding.action == BindingAction::ToggleDisplayMode)
.any(|binding| binding.modifiers.logo && binding.action == BindingAction::ToggleDisplayMode)
); );
} }
@@ -580,12 +608,9 @@ mod tests {
.iter() .iter()
.any(|binding| binding.trigger == BindingTrigger::Keysym(keysyms::KEY_F1)) .any(|binding| binding.trigger == BindingTrigger::Keysym(keysyms::KEY_F1))
); );
assert!( assert!(bindings.iter().any(|binding| binding.trigger
bindings == BindingTrigger::Keysym(keysyms::KEY_F2)
.iter() && binding.action == BindingAction::ToggleKeybindingGuide));
.any(|binding| binding.trigger == BindingTrigger::Keysym(keysyms::KEY_F2)
&& binding.action == BindingAction::ToggleKeybindingGuide)
);
} }
#[test] #[test]
fn workspace_back_and_forth_config_is_optional_and_bindable() { fn workspace_back_and_forth_config_is_optional_and_bindable() {
@@ -635,7 +660,12 @@ mod tests {
BindingAction::FocusMru(false), BindingAction::FocusMru(false),
BindingAction::FocusMru(true), BindingAction::FocusMru(true),
] { ] {
assert!(defaults.bindings.iter().any(|binding| binding.action == action)); assert!(
defaults
.bindings
.iter()
.any(|binding| binding.action == action)
);
} }
for (name, action) in [ for (name, action) in [
("focus-last-window", BindingAction::FocusLastWindow), ("focus-last-window", BindingAction::FocusLastWindow),
@@ -654,7 +684,8 @@ mod tests {
false, false,
true, true,
false false
) && binding.action == BindingAction::FocusMru(false))); ) && binding.action
== BindingAction::FocusMru(false)));
assert!(bindings.iter().any(|binding| binding.matches( assert!(bindings.iter().any(|binding| binding.matches(
23_u32.into(), 23_u32.into(),
&[keysyms::KEY_Tab], &[keysyms::KEY_Tab],
@@ -662,7 +693,8 @@ mod tests {
false, false,
true, true,
true true
) && binding.action == BindingAction::FocusMru(true))); ) && binding.action
== BindingAction::FocusMru(true)));
} }
#[test] #[test]
@@ -677,8 +709,14 @@ 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}\nautostart {\n command \"program\" \"argument with space\"\n}\n"; 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}\nautostart {\n command \"program\" \"argument with space\"\n}\n";
let config = Config::parse_source(source).unwrap(); let config = Config::parse_source(source).unwrap();
config.runtime_config().unwrap(); config.runtime_config().unwrap();
assert_eq!(config.input_settings().unwrap().touchpad.swipe_threshold, 96); assert_eq!(
assert_eq!(config.output_profiles().unwrap()[0].outputs[0].position, Some([0, 0])); config.input_settings().unwrap().touchpad.swipe_threshold,
96
);
assert_eq!(
config.output_profiles().unwrap()[0].outputs[0].position,
Some([0, 0])
);
} }
#[test] #[test]
@@ -716,7 +754,9 @@ mod tests {
); );
assert_eq!(config.xwayland, XwaylandConfig::default()); assert_eq!(config.xwayland, XwaylandConfig::default());
assert!(config.xwayland.enabled); assert!(config.xwayland.enabled);
let runtime = config.runtime_config().expect("the existing config stays valid"); let runtime = config
.runtime_config()
.expect("the existing config stays valid");
assert!(runtime.xwayland.enabled); assert!(runtime.xwayland.enabled);
} }
@@ -725,7 +765,10 @@ mod tests {
let config = parse(""); let config = parse("");
assert_eq!(config.layout_mode(), LayoutMode::Scrolling); 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()); assert_eq!(config.gap_config().unwrap(), GapConfig::default());
} }
@@ -735,7 +778,10 @@ mod tests {
let numeric = parse("scrolling {\n default-column-width 1.0\n}\n"); let numeric = parse("scrolling {\n default-column-width 1.0\n}\n");
assert_eq!(full.default_column_width().unwrap(), ColumnWidth::Full); 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] #[test]
@@ -759,10 +805,19 @@ mod tests {
let minimal = parse(""); let minimal = parse("");
let centered = parse("scrolling {\n focus-strategy \"center_on_focus\"\n}\n"); let centered = parse("scrolling {\n focus-strategy \"center_on_focus\"\n}\n");
let paged = parse("scrolling {\n focus-strategy \"paged\"\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!(
assert_eq!(centered.scrolling_focus_strategy(), ViewportFocusStrategy::Center); minimal.scrolling_focus_strategy(),
ViewportFocusStrategy::Minimal
);
assert_eq!(
centered.scrolling_focus_strategy(),
ViewportFocusStrategy::Center
);
} }
#[test] #[test]
@@ -804,7 +859,8 @@ mod tests {
#[test] #[test]
fn parses_and_validates_layout_gaps() { 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"); let invalid = parse("layout {\n outer-gap -1.0\n}\n");
assert_eq!( assert_eq!(
@@ -842,9 +898,11 @@ 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", "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_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_radius =
let invalid_opacity = parse("theme {\n geometry {\n window-radius -1.0\n }\n}\n");
parse("theme {\n shadow {\n soft {\n opacity 1.1\n }\n }\n}\n"); let invalid_opacity = parse(
"theme {\n shadow {\n soft {\n opacity 1.1\n }\n }\n}\n",
);
assert_eq!( assert_eq!(
configured.theme_settings().unwrap(), configured.theme_settings().unwrap(),
@@ -877,15 +935,24 @@ mod tests {
assert!(invalid_radius.theme_settings().is_err()); assert!(invalid_radius.theme_settings().is_err());
assert!(invalid_opacity.theme_settings().is_err()); assert!(invalid_opacity.theme_settings().is_err());
assert!( 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] #[test]
fn materials_default_to_solid_and_reject_removed_style() { 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!( 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()
); );
} }
@@ -932,9 +999,11 @@ mod tests {
"from \"#112233\"\nto \"#445566\"\nangle #inf\n", "from \"#112233\"\nto \"#445566\"\nangle #inf\n",
] { ] {
assert!( assert!(
Config::parse_source(&format!("theme {{ focus-ring {{ gradient {{\n{settings}\n}} }} }}")) Config::parse_source(&format!(
.and_then(|config| config.theme_settings()) "theme {{ focus-ring {{ gradient {{\n{settings}\n}} }} }}"
.is_err() ))
.and_then(|config| config.theme_settings())
.is_err()
); );
} }
assert!( assert!(
@@ -1023,17 +1092,21 @@ mod tests {
for key in ["active-opacity", "inactive-opacity", "inactive-dim"] { for key in ["active-opacity", "inactive-opacity", "inactive-dim"] {
for value in ["-0.1", "1.1", "#nan", "#inf"] { for value in ["-0.1", "1.1", "#nan", "#inf"] {
assert!( assert!(
Config::parse_source(&format!("appearance {{ focus-effect {{ {key} {value}; }} }}")) Config::parse_source(&format!(
.and_then(|config| config.runtime_config()) "appearance {{ focus-effect {{ {key} {value}; }} }}"
.is_err() ))
.and_then(|config| config.runtime_config())
.is_err()
); );
} }
} }
for value in ["-1", "#nan", "#inf"] { for value in ["-1", "#nan", "#inf"] {
assert!( assert!(
Config::parse_source(&format!("appearance {{ focus-effect {{ duration-ms {value}; }} }}")) Config::parse_source(&format!(
.and_then(|config| config.runtime_config()) "appearance {{ focus-effect {{ duration-ms {value}; }} }}"
.is_err() ))
.and_then(|config| config.runtime_config())
.is_err()
); );
} }
let disabled = parse("appearance { focus-effect { enabled #false; active-opacity 0.9; inactive-opacity 0.8; inactive-dim 0.25; }; }") let disabled = parse("appearance { focus-effect { enabled #false; active-opacity 0.9; inactive-opacity 0.8; inactive-dim 0.25; }; }")
@@ -1109,9 +1182,11 @@ mod tests {
); );
for threshold in [0, 15, 1001] { for threshold in [0, 15, 1001] {
assert!( assert!(
parse(&format!("input {{ touchpad {{ swipe-threshold {threshold}; }} }}")) parse(&format!(
.input_settings() "input {{ touchpad {{ swipe-threshold {threshold}; }} }}"
.is_err() ))
.input_settings()
.is_err()
); );
} }
} }
@@ -1133,8 +1208,9 @@ mod tests {
#[test] #[test]
fn guide_uses_effective_bindings_including_overrides_and_unbindings() { fn guide_uses_effective_bindings_including_overrides_and_unbindings() {
let config = let config = parse(
parse("binding keys=\"Super+Q\" action=\"none\"\nbinding keys=\"Super+Tab\" action=\"toggle-floating\"\n"); "binding keys=\"Super+Q\" action=\"none\"\nbinding keys=\"Super+Tab\" action=\"toggle-floating\"\n",
);
let input = config.input_settings().unwrap(); let input = config.input_settings().unwrap();
let map = physical_keymap(&input).unwrap(); let map = physical_keymap(&input).unwrap();
let entries = config let entries = config
@@ -1143,12 +1219,13 @@ mod tests {
.iter() .iter()
.filter_map(|binding| binding.guide_entry(Some(&map))) .filter_map(|binding| binding.guide_entry(Some(&map)))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
assert!(!entries.iter().any(|entry| entry["description"] == "Close window"));
assert!( assert!(
entries !entries
.iter() .iter()
.any(|entry| entry["keys"] == "Super + Tab" && entry["description"] == "Toggle floating window") .any(|entry| entry["description"] == "Close window")
); );
assert!(entries.iter().any(|entry| entry["keys"] == "Super + Tab"
&& entry["description"] == "Toggle floating window"));
assert!(entries.iter().any(|entry| entry["keys"] == "Super + Enter")); assert!(entries.iter().any(|entry| entry["keys"] == "Super + Enter"));
} }
@@ -1157,7 +1234,11 @@ mod tests {
let defaults = Config::default().runtime_config().unwrap(); let defaults = Config::default().runtime_config().unwrap();
for (key, name, action) in [ for (key, name, action) in [
(keysyms::KEY_XF86AudioPlay, "media-play-pause", "play-pause"), (keysyms::KEY_XF86AudioPlay, "media-play-pause", "play-pause"),
(keysyms::KEY_XF86AudioPause, "media-play-pause", "play-pause"), (
keysyms::KEY_XF86AudioPause,
"media-play-pause",
"play-pause",
),
(keysyms::KEY_XF86AudioNext, "media-next", "next"), (keysyms::KEY_XF86AudioNext, "media-next", "next"),
(keysyms::KEY_XF86AudioPrev, "media-previous", "previous"), (keysyms::KEY_XF86AudioPrev, "media-previous", "previous"),
] { ] {
@@ -1186,7 +1267,9 @@ mod tests {
parse("scrolling { default-column-display \"tabbed\"; }\n").default_column_display(), parse("scrolling { default-column-display \"tabbed\"; }\n").default_column_display(),
ferese_layout::ColumnDisplay::Tabbed ferese_layout::ColumnDisplay::Tabbed
); );
assert!(Config::parse_source("scrolling { default-column-display \"unknown\"; }\n").is_err()); assert!(
Config::parse_source("scrolling { default-column-display \"unknown\"; }\n").is_err()
);
} }
#[test] #[test]
@@ -1224,7 +1307,11 @@ mod tests {
&& binding.trigger == BindingTrigger::Keysym(keysyms::KEY_f) && binding.trigger == BindingTrigger::Keysym(keysyms::KEY_f)
&& binding.action == action)); && 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 assert!(bindings.iter().any(|binding| binding.modifiers.logo
&& !binding.modifiers.shift && !binding.modifiers.shift
&& binding.trigger == BindingTrigger::Keysym(keysyms::KEY_space) && binding.trigger == BindingTrigger::Keysym(keysyms::KEY_space)
@@ -1242,13 +1329,16 @@ mod tests {
binding.modifiers.logo binding.modifiers.logo
&& binding.modifiers.shift && binding.modifiers.shift
&& binding.trigger == BindingTrigger::Keysym(keysyms::KEY_s) && binding.trigger == BindingTrigger::Keysym(keysyms::KEY_s)
&& binding.action == BindingAction::Spawn(vec!["ferese-screenshot".to_owned()].into()) && binding.action
== BindingAction::Spawn(vec!["ferese-screenshot".to_owned()].into())
})); }));
assert!(bindings.iter().any(|binding| { assert!(bindings.iter().any(|binding| {
binding.trigger == BindingTrigger::Keysym(keysyms::KEY_Print) binding.trigger == BindingTrigger::Keysym(keysyms::KEY_Print)
&& binding.modifiers == BindingModifiers::default() && binding.modifiers == BindingModifiers::default()
&& binding.action && binding.action
== BindingAction::Spawn(vec!["ferese-screenshot".to_owned(), "--full".to_owned()].into()) == BindingAction::Spawn(
vec!["ferese-screenshot".to_owned(), "--full".to_owned()].into(),
)
})); }));
assert!(bindings.iter().any(|binding| { assert!(bindings.iter().any(|binding| {
binding.modifiers.logo binding.modifiers.logo
@@ -1269,18 +1359,26 @@ mod tests {
assert_eq!(bindings.len(), 63); assert_eq!(bindings.len(), 63);
assert!(bindings.iter().any(|binding| { assert!(bindings.iter().any(|binding| {
binding.action binding.action
== BindingAction::Spawn(vec!["foot".to_owned(), "--app-id".to_owned(), "work".to_owned()].into()) == BindingAction::Spawn(
vec!["foot".to_owned(), "--app-id".to_owned(), "work".to_owned()].into(),
)
})); }));
let input = unbound.input_settings().unwrap(); let input = unbound.input_settings().unwrap();
let bindings = unbound.bindings(&input).unwrap(); let bindings = unbound.bindings(&input).unwrap();
assert_eq!(bindings.len(), 62); assert_eq!(bindings.len(), 62);
assert!(!bindings.iter().any(|binding| binding.action == BindingAction::Close)); assert!(
!bindings
.iter()
.any(|binding| binding.action == BindingAction::Close)
);
} }
#[test] #[test]
fn accepts_physical_xkb_key_names() { 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 input = config.input_settings().unwrap();
let bindings = config.bindings(&input).unwrap(); let bindings = config.bindings(&input).unwrap();
@@ -1292,9 +1390,13 @@ mod tests {
#[test] #[test]
fn rejects_duplicate_or_invalid_user_bindings() { fn rejects_duplicate_or_invalid_user_bindings() {
let duplicate = parse("binding keys=\"Super+Q\" action=\"close\"\nbinding keys=\"logo+q\" action=\"close\"\n"); let duplicate = parse(
let missing_command = parse("binding keys=\"Super+Enter\" action=\"spawn\" argument=\"missing\"\n"); "binding keys=\"Super+Q\" action=\"close\"\nbinding keys=\"logo+q\" action=\"close\"\n",
let invalid_argument = parse("binding keys=\"Super+Q\" action=\"close\" argument=\"left\"\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] { for config in [duplicate, missing_command, invalid_argument] {
let input = config.input_settings().unwrap(); let input = config.input_settings().unwrap();
@@ -1308,7 +1410,12 @@ mod tests {
"window-rule app-id=\"org.example.Editor\" workspace=3 floating=#true width=900.0 height=600.0 min-width=500 min-height=400 fullscreen=#false block-out-from-screencasts=#true\n", "window-rule app-id=\"org.example.Editor\" workspace=3 floating=#true width=900.0 height=600.0 min-width=500 min-height=400 fullscreen=#false block-out-from-screencasts=#true\n",
); );
let rules = config.window_rules().unwrap(); 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.workspace, Some(3));
assert_eq!(result.floating, Some(true)); assert_eq!(result.floating, Some(true));
@@ -1406,24 +1513,34 @@ mod tests {
] { ] {
assert!(parse(source).output_profiles().is_err(), "{source}"); assert!(parse(source).output_profiles().is_err(), "{source}");
} }
assert!(Config::parse_source("output-profile a layout=\"docked\" { output eDP-1; }").is_err()); assert!(
Config::parse_source("output-profile a layout=\"docked\" { output eDP-1; }").is_err()
);
} }
#[test] #[test]
fn automatic_refresh_is_opt_in_and_rejects_wrong_types() { fn automatic_refresh_is_opt_in_and_rejects_wrong_types() {
let config = parse("output-profile laptop { output eDP-1 mode=\"2880x1800@120\" auto-refresh=#true; }"); let config = parse(
"output-profile laptop { output eDP-1 mode=\"2880x1800@120\" auto-refresh=#true; }",
);
assert!(config.output_profiles().unwrap()[0].outputs[0].auto_refresh); assert!(config.output_profiles().unwrap()[0].outputs[0].auto_refresh);
let manual = parse("output-profile laptop { output eDP-1 mode=\"2880x1800@60\"; }"); let manual = parse("output-profile laptop { output eDP-1 mode=\"2880x1800@60\"; }");
assert!(!manual.output_profiles().unwrap()[0].outputs[0].auto_refresh); assert!(!manual.output_profiles().unwrap()[0].outputs[0].auto_refresh);
assert!(Config::parse_source("output-profile laptop { output eDP-1 auto-refresh=\"yes\"; }").is_err()); assert!(
Config::parse_source("output-profile laptop { output eDP-1 auto-refresh=\"yes\"; }")
.is_err()
);
} }
#[test] #[test]
fn rejects_invalid_output_profiles() { 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_scale =
let invalid_mode = parse("output-profile name=\"bad\" {\n output match=\"eDP-1\" mode=\"native\"\n}\n"); parse("output-profile name=\"bad\" {\n output match=\"eDP-1\" scale=0.0\n}\n");
let duplicate = let invalid_mode =
parse("output-profile name=\"bad\" {\n output match=\"eDP-1\"\n output match=\"eDP-1\"\n}\n"); 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_scale.output_profiles().is_err());
assert!(invalid_mode.output_profiles().is_err()); assert!(invalid_mode.output_profiles().is_err());
+54 -16
View File
@@ -6,21 +6,39 @@ impl Config {
Ok(FocusEffectSettings { Ok(FocusEffectSettings {
enabled: effect.enabled, enabled: effect.enabled,
blur: effect.blur, blur: effect.blur,
active_opacity: unit_theme_value(effect.active_opacity, "appearance.focus_effect.active_opacity")?, active_opacity: unit_theme_value(
inactive_opacity: unit_theme_value(effect.inactive_opacity, "appearance.focus_effect.inactive_opacity")?, effect.active_opacity,
inactive_dim: unit_theme_value(effect.inactive_dim, "appearance.focus_effect.inactive_dim")?, "appearance.focus_effect.active_opacity",
duration_ms: nonnegative_theme_value(effect.duration_ms, "appearance.focus_effect.duration_ms")?, )?,
inactive_opacity: unit_theme_value(
effect.inactive_opacity,
"appearance.focus_effect.inactive_opacity",
)?,
inactive_dim: unit_theme_value(
effect.inactive_dim,
"appearance.focus_effect.inactive_dim",
)?,
duration_ms: nonnegative_theme_value(
effect.duration_ms,
"appearance.focus_effect.duration_ms",
)?,
}) })
} }
pub fn theme_settings(&self) -> Result<ThemeSettings, ConfigError> { pub fn theme_settings(&self) -> Result<ThemeSettings, ConfigError> {
let border_width = nonnegative_theme_value(self.theme.geometry.border_width, "geometry.border_width")?; let border_width =
let focus_ring_width = nonnegative_theme_value(self.theme.geometry.border_width, "geometry.border_width")?;
nonnegative_theme_value(self.theme.geometry.focus_ring_width, "geometry.focus_ring_width")?; let focus_ring_width = nonnegative_theme_value(
let window_radius = nonnegative_theme_value(self.theme.geometry.window_radius, "geometry.window_radius")?; self.theme.geometry.focus_ring_width,
let shadow_offset_y = finite_theme_value(self.theme.shadow.soft.offset_y, "shadow.soft.offset_y")?; "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_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( let material_radius = nonnegative_theme_value(
self.theme.geometry.shell_radius.unwrap_or(14.0), self.theme.geometry.shell_radius.unwrap_or(14.0),
@@ -30,13 +48,22 @@ impl Config {
border_width, border_width,
focus_ring_width, focus_ring_width,
border_color: parse_color(&self.theme.colors.border, "colors.border")?, 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")?, accent_color: parse_color(&self.theme.colors.accent, "colors.accent")?,
border_gradient: self.theme.border.settings("border")?, border_gradient: self.theme.border.settings("border")?,
focus_ring_gradient: self.theme.focus_ring.settings("focus_ring")?, focus_ring_gradient: self.theme.focus_ring.settings("focus_ring")?,
shadow_color: parse_color(&self.theme.colors.shadow, "colors.shadow")?, 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(
surface_raised_color: parse_color(&self.theme.colors.surface_raised, "colors.surface_raised")?, &self.theme.colors.surface_base,
"colors.surface_base",
)?,
surface_raised_color: parse_color(
&self.theme.colors.surface_raised,
"colors.surface_raised",
)?,
bar_background_color: parse_color( bar_background_color: parse_color(
self.theme self.theme
.surface .surface
@@ -52,8 +79,15 @@ impl Config {
shadow_blur, shadow_blur,
shadow_opacity, shadow_opacity,
material_style: self.theme.material.style, 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(
material_tint_strength: unit_theme_value(self.theme.material.tint_strength, "material.tint_strength")?, self.theme.material.blur_radius,
"material.blur_radius",
)?
.min(32.0),
material_tint_strength: unit_theme_value(
self.theme.material.tint_strength,
"material.tint_strength",
)?,
material_radius, material_radius,
panel_radius: material_radius, panel_radius: material_radius,
reduce_transparency: false, reduce_transparency: false,
@@ -189,7 +223,11 @@ impl BorderPaintConfig {
"focus_ring.gradient.angle", "focus_ring.gradient.angle",
) )
} else { } else {
("border.gradient.from", "border.gradient.to", "border.gradient.angle") (
"border.gradient.from",
"border.gradient.to",
"border.gradient.angle",
)
}; };
let gradient = self let gradient = self
.gradient .gradient
+129 -40
View File
@@ -12,7 +12,10 @@ impl Config {
fn binding_commands(&self) -> HashMap<String, Vec<String>> { fn binding_commands(&self) -> HashMap<String, Vec<String>> {
let mut commands = HashMap::from([ let mut commands = HashMap::from([
("terminal".to_owned(), vec!["foot".to_owned()]), ("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(), "screenshot-full".to_owned(),
vec!["ferese-screenshot".to_owned(), "--full".to_owned()], vec!["ferese-screenshot".to_owned(), "--full".to_owned()],
@@ -36,7 +39,10 @@ impl Config {
for configured in &self.bindings { for configured in &self.bindings {
let identity = binding_identity(configured, &keymap)?; let identity = binding_identity(configured, &keymap)?;
if !supplied.insert(identity.clone()) { if !supplied.insert(identity.clone()) {
return Err(ConfigError::Binding(format!("duplicate binding {:?}", configured.keys))); return Err(ConfigError::Binding(format!(
"duplicate binding {:?}",
configured.keys
)));
} }
bindings.retain(|binding| binding_identity_for_runtime(binding) != identity); bindings.retain(|binding| binding_identity_for_runtime(binding) != identity);
@@ -56,13 +62,19 @@ impl Config {
} }
} }
pub(super) fn validate_commands(commands: &HashMap<String, Vec<String>>) -> Result<(), ConfigError> { pub(super) fn validate_commands(
commands: &HashMap<String, Vec<String>>,
) -> Result<(), ConfigError> {
for (name, argv) in commands { for (name, argv) in commands {
if name.trim().is_empty() { if name.trim().is_empty() {
return Err(ConfigError::Binding("command names cannot be empty".to_owned())); return Err(ConfigError::Binding(
"command names cannot be empty".to_owned(),
));
} }
if argv.is_empty() || argv[0].is_empty() { if argv.is_empty() || argv[0].is_empty() {
return Err(ConfigError::Binding(format!("command {name:?} must contain a program"))); return Err(ConfigError::Binding(format!(
"command {name:?} must contain a program"
)));
} }
} }
@@ -98,10 +110,9 @@ pub(super) fn parse_binding(
} }
let identity = binding_identity(configured, keymap)?; let identity = binding_identity(configured, keymap)?;
let action = configured let action = configured.action.as_deref().ok_or_else(|| {
.action ConfigError::Binding(format!("binding {:?} has no action", configured.keys))
.as_deref() })?;
.ok_or_else(|| ConfigError::Binding(format!("binding {:?} has no action", configured.keys)))?;
let action = parse_action(action, configured.argument.as_deref(), commands)?; let action = parse_action(action, configured.argument.as_deref(), commands)?;
let trigger = match identity.match_mode { let trigger = match identity.match_mode {
BindingMatch::Keysym => BindingTrigger::Keysym(identity.key), BindingMatch::Keysym => BindingTrigger::Keysym(identity.key),
@@ -130,7 +141,9 @@ pub(super) fn binding_identity(
) -> Result<BindingIdentity, ConfigError> { ) -> Result<BindingIdentity, ConfigError> {
let (modifiers, key) = parse_chord(&configured.keys)?; let (modifiers, key) = parse_chord(&configured.keys)?;
if let Some((fingers, direction)) = parse_swipe(&key) { 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::Binding( return Err(ConfigError::Binding(
"swipes cannot use keyboard modifiers or physical key matching".into(), "swipes cannot use keyboard modifiers or physical key matching".into(),
)); ));
@@ -170,7 +183,9 @@ pub(super) fn binding_identity_for_runtime(binding: &Binding) -> BindingIdentity
let (match_mode, key) = match binding.trigger { let (match_mode, key) = match binding.trigger {
BindingTrigger::Keysym(key) => (BindingMatch::Keysym, key), BindingTrigger::Keysym(key) => (BindingMatch::Keysym, key),
BindingTrigger::Physical(key) => (BindingMatch::Physical, key.raw()), 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 { BindingIdentity {
@@ -198,7 +213,9 @@ pub(super) fn parse_chord(chord: &str) -> Result<(BindingModifiers, String), Con
}; };
if let Some(slot) = slot { if let Some(slot) = slot {
if *slot { if *slot {
return Err(ConfigError::Binding(format!("duplicate modifier in {chord:?}"))); return Err(ConfigError::Binding(format!(
"duplicate modifier in {chord:?}"
)));
} }
*slot = true; *slot = true;
} else if key.replace(component.to_owned()).is_some() { } else if key.replace(component.to_owned()).is_some() {
@@ -208,7 +225,9 @@ pub(super) fn parse_chord(chord: &str) -> Result<(BindingModifiers, String), Con
} }
} }
let key = key.ok_or_else(|| ConfigError::Binding(format!("key chord {chord:?} does not contain a key")))?; let key = key.ok_or_else(|| {
ConfigError::Binding(format!("key chord {chord:?} does not contain a key"))
})?;
Ok((modifiers, key)) Ok((modifiers, key))
} }
@@ -264,8 +283,10 @@ pub(super) fn parse_action(
Ok(()) Ok(())
} }
}; };
let required_argument = let required_argument = || {
|| argument.ok_or_else(|| ConfigError::Binding(format!("action {action:?} requires an argument"))); argument
.ok_or_else(|| ConfigError::Binding(format!("action {action:?} requires an argument")))
};
match action { match action {
"binding-mode" => Ok(BindingAction::BindingMode(required_argument()?.to_owned())), "binding-mode" => Ok(BindingAction::BindingMode(required_argument()?.to_owned())),
@@ -275,7 +296,9 @@ pub(super) fn parse_action(
} }
"workspace-next" | "workspace-previous" => { "workspace-next" | "workspace-previous" => {
no_argument()?; no_argument()?;
Ok(BindingAction::SwitchRelativeWorkspace(action == "workspace-next")) Ok(BindingAction::SwitchRelativeWorkspace(
action == "workspace-next",
))
} }
"workspace-back-and-forth" => { "workspace-back-and-forth" => {
no_argument()?; no_argument()?;
@@ -310,9 +333,15 @@ pub(super) fn parse_action(
} }
"focus" => Ok(BindingAction::Focus(parse_direction(required_argument()?)?)), "focus" => Ok(BindingAction::Focus(parse_direction(required_argument()?)?)),
"move" => Ok(BindingAction::Move(parse_direction(required_argument()?)?)), "move" => Ok(BindingAction::Move(parse_direction(required_argument()?)?)),
"resize" => Ok(BindingAction::Resize(parse_direction(required_argument()?)?)), "resize" => Ok(BindingAction::Resize(
"workspace" => Ok(BindingAction::SwitchWorkspace(parse_workspace(required_argument()?)?)), parse_direction(required_argument()?)?,
"move-to-workspace" => Ok(BindingAction::MoveToWorkspace(parse_workspace(required_argument()?)?)), )),
"workspace" => Ok(BindingAction::SwitchWorkspace(parse_workspace(
required_argument()?,
)?)),
"move-to-workspace" => Ok(BindingAction::MoveToWorkspace(parse_workspace(
required_argument()?,
)?)),
"toggle-maximized" => { "toggle-maximized" => {
no_argument()?; no_argument()?;
Ok(BindingAction::ToggleMaximized) Ok(BindingAction::ToggleMaximized)
@@ -337,7 +366,9 @@ pub(super) fn parse_action(
no_argument()?; no_argument()?;
Ok(BindingAction::ToggleColumnTabbed) Ok(BindingAction::ToggleColumnTabbed)
} }
"join-tab" => Ok(BindingAction::JoinTab(parse_direction(required_argument()?)?)), "join-tab" => Ok(BindingAction::JoinTab(parse_direction(
required_argument()?
)?)),
"leave-tab" => { "leave-tab" => {
no_argument()?; no_argument()?;
Ok(BindingAction::LeaveTab) Ok(BindingAction::LeaveTab)
@@ -396,14 +427,18 @@ pub(super) fn parse_direction(argument: &str) -> Result<Direction, ConfigError>
"right" => Ok(Direction::Right), "right" => Ok(Direction::Right),
"up" => Ok(Direction::Up), "up" => Ok(Direction::Up),
"down" => Ok(Direction::Down), "down" => Ok(Direction::Down),
_ => Err(ConfigError::Binding(format!("invalid direction {argument:?}"))), _ => Err(ConfigError::Binding(format!(
"invalid direction {argument:?}"
))),
} }
} }
pub(super) fn parse_workspace(argument: &str) -> Result<u8, ConfigError> { pub(super) fn parse_workspace(argument: &str) -> Result<u8, ConfigError> {
match argument.parse() { match argument.parse() {
Ok(workspace) if workspace > 0 => Ok(workspace), Ok(workspace) if workspace > 0 => Ok(workspace),
_ => Err(ConfigError::Binding(format!("invalid workspace {argument:?}"))), _ => Err(ConfigError::Binding(format!(
"invalid workspace {argument:?}"
))),
} }
} }
@@ -446,13 +481,25 @@ pub(super) fn default_bindings() -> Vec<BindingConfig> {
for (key, direction) in [("H", "left"), ("J", "down"), ("K", "up"), ("L", "right")] { 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+".to_owned() + key, "focus", Some(direction)));
bindings.push(binding("Super+Shift+".to_owned() + key, "move", Some(direction))); bindings.push(binding(
bindings.push(binding("Super+Ctrl+".to_owned() + key, "resize", Some(direction))); "Super+Shift+".to_owned() + key,
"move",
Some(direction),
));
bindings.push(binding(
"Super+Ctrl+".to_owned() + key,
"resize",
Some(direction),
));
} }
for workspace in 1..=9 { for workspace in 1..=9 {
let workspace = workspace.to_string(); 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( bindings.push(binding(
format!("Super+Shift+{workspace}"), format!("Super+Shift+{workspace}"),
"move-to-workspace", "move-to-workspace",
@@ -463,7 +510,11 @@ pub(super) fn default_bindings() -> Vec<BindingConfig> {
bindings bindings
} }
pub(super) fn binding(keys: impl Into<String>, action: impl Into<String>, argument: Option<&str>) -> BindingConfig { pub(super) fn binding(
keys: impl Into<String>,
action: impl Into<String>,
argument: Option<&str>,
) -> BindingConfig {
BindingConfig { BindingConfig {
keys: keys.into(), keys: keys.into(),
match_mode: BindingMatch::Keysym, match_mode: BindingMatch::Keysym,
@@ -557,7 +608,10 @@ impl Binding {
let symbol = parse_keysym(&key)?; let symbol = parse_keysym(&key)?;
if (modifiers.ctrl if (modifiers.ctrl
&& modifiers.alt && 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) || (keysyms::KEY_XF86Switch_VT_1..=keysyms::KEY_XF86Switch_VT_12).contains(&symbol)
{ {
return Err(ConfigError::Binding("Reserved system shortcut".into())); return Err(ConfigError::Binding("Reserved system shortcut".into()));
@@ -576,19 +630,34 @@ impl Binding {
match (&self.trigger, &other.trigger) { match (&self.trigger, &other.trigger) {
(BindingTrigger::Keysym(left), BindingTrigger::Keysym(right)) => left == right, (BindingTrigger::Keysym(left), BindingTrigger::Keysym(right)) => left == right,
(BindingTrigger::Keysym(symbol), BindingTrigger::Physical(code)) (BindingTrigger::Keysym(symbol), BindingTrigger::Physical(code))
| (BindingTrigger::Physical(code), BindingTrigger::Keysym(symbol)) => (0..map.num_layouts_for_key(*code)) | (BindingTrigger::Physical(code), BindingTrigger::Keysym(symbol)) => {
.any(|layout| { (0..map.num_layouts_for_key(*code)).any(|layout| {
map.key_get_syms_by_level(*code, layout, 0) map.key_get_syms_by_level(*code, layout, 0)
.iter() .iter()
.any(|key| key.raw() == *symbol) .any(|key| key.raw() == *symbol)
}), })
}
(BindingTrigger::Physical(left), BindingTrigger::Physical(right)) => left == right, (BindingTrigger::Physical(left), BindingTrigger::Physical(right)) => left == right,
_ => false, _ => false,
} }
} }
pub fn matches(&self, keycode: Keycode, keysyms: &[u32], logo: bool, ctrl: bool, alt: bool, shift: bool) -> bool { pub fn matches(
self.modifiers == BindingModifiers { logo, ctrl, alt, shift } &self,
keycode: Keycode,
keysyms: &[u32],
logo: bool,
ctrl: bool,
alt: bool,
shift: bool,
) -> bool {
self.modifiers
== BindingModifiers {
logo,
ctrl,
alt,
shift,
}
&& match self.trigger { && match self.trigger {
BindingTrigger::Keysym(expected) => keysyms.contains(&expected), BindingTrigger::Keysym(expected) => keysyms.contains(&expected),
BindingTrigger::Physical(expected) => expected == keycode, BindingTrigger::Physical(expected) => expected == keycode,
@@ -600,7 +669,11 @@ impl Binding {
let description = match &self.action { let description = match &self.action {
BindingAction::BindingMode(name) => format!("Switch to binding mode {name}"), BindingAction::BindingMode(name) => format!("Switch to binding mode {name}"),
BindingAction::None => return None, 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") { if argv.iter().any(|arg| arg == "--full") {
"Capture whole screen" "Capture whole screen"
} else { } else {
@@ -619,7 +692,9 @@ impl Binding {
BindingAction::Exit => "Log out (with confirmation)".into(), BindingAction::Exit => "Log out (with confirmation)".into(),
BindingAction::Focus(direction) => format!("Focus window {direction:?}"), BindingAction::Focus(direction) => format!("Focus window {direction:?}"),
BindingAction::FocusLastWindow => "Return to last focused window".into(), BindingAction::FocusLastWindow => "Return to last focused window".into(),
BindingAction::FocusFloating => "Switch focus between tiled and floating windows".into(), BindingAction::FocusFloating => {
"Switch focus between tiled and floating windows".into()
}
BindingAction::FocusMru(reverse) => if *reverse { BindingAction::FocusMru(reverse) => if *reverse {
"Previous window in focus history" "Previous window in focus history"
} else { } else {
@@ -629,11 +704,16 @@ impl Binding {
BindingAction::Move(direction) => format!("Move window {direction:?}"), BindingAction::Move(direction) => format!("Move window {direction:?}"),
BindingAction::Resize(direction) => format!("Resize window {direction:?}"), BindingAction::Resize(direction) => format!("Resize window {direction:?}"),
BindingAction::SwitchWorkspace(index) => format!("Go to workspace {index}"), BindingAction::SwitchWorkspace(index) => format!("Go to workspace {index}"),
BindingAction::WorkspaceBackAndForth => "Return to last workspace on this monitor".into(), BindingAction::WorkspaceBackAndForth => {
BindingAction::MoveToWorkspace(index) => format!("Move window to workspace {index}"), "Return to last workspace on this monitor".into()
BindingAction::SwitchRelativeWorkspace(next) => {
if *next { "Next workspace" } else { "Previous workspace" }.into()
} }
BindingAction::MoveToWorkspace(index) => format!("Move window to workspace {index}"),
BindingAction::SwitchRelativeWorkspace(next) => if *next {
"Next workspace"
} else {
"Previous workspace"
}
.into(),
BindingAction::ToggleFullscreen => "Toggle fullscreen".into(), BindingAction::ToggleFullscreen => "Toggle fullscreen".into(),
BindingAction::ToggleMaximized => "Toggle full-width window".into(), BindingAction::ToggleMaximized => "Toggle full-width window".into(),
BindingAction::ToggleLayout => "Switch workspace layout".into(), BindingAction::ToggleLayout => "Switch workspace layout".into(),
@@ -642,7 +722,12 @@ impl Binding {
BindingAction::ToggleColumnTabbed => "Toggle stacked / tabbed column".into(), BindingAction::ToggleColumnTabbed => "Toggle stacked / tabbed column".into(),
BindingAction::JoinTab(direction) => format!("Join tab group {direction:?}"), BindingAction::JoinTab(direction) => format!("Join tab group {direction:?}"),
BindingAction::LeaveTab => "Leave tab group".into(), BindingAction::LeaveTab => "Leave tab group".into(),
BindingAction::CycleTab(previous) => if *previous { "Previous tab" } else { "Next tab" }.into(), BindingAction::CycleTab(previous) => if *previous {
"Previous tab"
} else {
"Next tab"
}
.into(),
BindingAction::Consume => "Join window into column or tab group".into(), BindingAction::Consume => "Join window into column or tab group".into(),
BindingAction::Expel => "Leave column or tab group".into(), BindingAction::Expel => "Leave column or tab group".into(),
BindingAction::ToggleFloating => "Toggle floating window".into(), BindingAction::ToggleFloating => "Toggle floating window".into(),
@@ -694,7 +779,11 @@ impl Binding {
matches!(self.trigger, BindingTrigger::Swipe { fingers, .. } if fingers == count) 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) matches!(self.trigger, BindingTrigger::Swipe { fingers, direction } if fingers == count && direction == target)
} }
} }
+28 -8
View File
@@ -42,7 +42,12 @@ impl BindingSet {
alt: bool, alt: bool,
shift: bool, shift: bool,
) -> Option<&Binding> { ) -> Option<&Binding> {
let modifiers = BindingModifiers { logo, ctrl, alt, shift }; let modifiers = BindingModifiers {
logo,
ctrl,
alt,
shift,
};
let physical = self.keyboard.get(&BindingIdentity { let physical = self.keyboard.get(&BindingIdentity {
modifiers, modifiers,
match_mode: BindingMatch::Physical, match_mode: BindingMatch::Physical,
@@ -96,7 +101,10 @@ mod tests {
let code = Keycode::new(38); let code = Keycode::new(38);
let bindings = vec![ let bindings = vec![
binding_for(BindingTrigger::Keysym(keysyms::KEY_b), BindingAction::Close), binding_for(BindingTrigger::Keysym(keysyms::KEY_b), BindingAction::Close),
binding_for(BindingTrigger::Physical(code), BindingAction::ToggleFloating), binding_for(
BindingTrigger::Physical(code),
BindingAction::ToggleFloating,
),
binding_for(BindingTrigger::Keysym(keysyms::KEY_a), BindingAction::Exit), binding_for(BindingTrigger::Keysym(keysyms::KEY_a), BindingAction::Exit),
binding_for(BindingTrigger::Keysym(keysyms::KEY_b), BindingAction::None), binding_for(BindingTrigger::Keysym(keysyms::KEY_b), BindingAction::None),
]; ];
@@ -125,7 +133,10 @@ mod tests {
let config = Config::default(); let config = Config::default();
let mut bindings = config.bindings(&config.input_settings().unwrap()).unwrap(); let mut bindings = config.bindings(&config.input_settings().unwrap()).unwrap();
bindings.extend([ bindings.extend([
binding_for(BindingTrigger::Physical(Keycode::new(38)), BindingAction::None), binding_for(
BindingTrigger::Physical(Keycode::new(38)),
BindingAction::None,
),
binding_for(BindingTrigger::Keysym(keysyms::KEY_a), BindingAction::Close), binding_for(BindingTrigger::Keysym(keysyms::KEY_a), BindingAction::Close),
binding_for( binding_for(
BindingTrigger::Swipe { BindingTrigger::Swipe {
@@ -138,7 +149,8 @@ mod tests {
let index = BindingSet::from(bindings); let index = BindingSet::from(bindings);
for bits in 0..16 { for bits in 0..16 {
let (logo, ctrl, alt, shift) = (bits & 1 != 0, bits & 2 != 0, bits & 4 != 0, bits & 8 != 0); let (logo, ctrl, alt, shift) =
(bits & 1 != 0, bits & 2 != 0, bits & 4 != 0, bits & 8 != 0);
for code in 8..100 { for code in 8..100 {
for symbols in [ for symbols in [
@@ -164,7 +176,10 @@ mod tests {
#[test] #[test]
fn replacing_bindings_discards_old_index_entries() { fn replacing_bindings_discards_old_index_entries() {
let code = Keycode::new(38); let code = Keycode::new(38);
let mut bindings = BindingSet::from(vec![binding_for(BindingTrigger::Physical(code), BindingAction::Close)]); let mut bindings = BindingSet::from(vec![binding_for(
BindingTrigger::Physical(code),
BindingAction::Close,
)]);
assert_eq!( assert_eq!(
bindings.action_for_key(code, &[], false, false, false, false), bindings.action_for_key(code, &[], false, false, false, false),
Some(&BindingAction::Close) Some(&BindingAction::Close)
@@ -173,7 +188,10 @@ mod tests {
BindingTrigger::Keysym(keysyms::KEY_b), BindingTrigger::Keysym(keysyms::KEY_b),
BindingAction::None, BindingAction::None,
)]); )]);
assert_eq!(bindings.action_for_key(code, &[], false, false, false, false), None); assert_eq!(
bindings.action_for_key(code, &[], false, false, false, false),
None
);
assert_eq!( assert_eq!(
bindings.action_for_key(code, &[keysyms::KEY_b], false, false, false, false), bindings.action_for_key(code, &[keysyms::KEY_b], false, false, false, false),
Some(&BindingAction::None) Some(&BindingAction::None)
@@ -182,9 +200,11 @@ mod tests {
#[test] #[test]
fn cloning_spawn_actions_shares_the_command_storage() { fn cloning_spawn_actions_shares_the_command_storage() {
let action = BindingAction::Spawn(vec!["foot".into(), "--app-id".into(), "work".into()].into()); let action =
BindingAction::Spawn(vec!["foot".into(), "--app-id".into(), "work".into()].into());
let cloned = action.clone(); let cloned = action.clone();
let (BindingAction::Spawn(original), BindingAction::Spawn(cloned)) = (&action, cloned) else { let (BindingAction::Spawn(original), BindingAction::Spawn(cloned)) = (&action, cloned)
else {
unreachable!(); unreachable!();
}; };
assert!(Arc::ptr_eq(original, &cloned)); assert!(Arc::ptr_eq(original, &cloned));
+48 -14
View File
@@ -75,16 +75,24 @@ impl Config {
if configured.disabled if configured.disabled
|| identity.match_mode == BindingMatch::Swipe || identity.match_mode == BindingMatch::Swipe
|| !identities.insert(identity.clone()) || !identities.insert(identity.clone())
|| (identity.match_mode == BindingMatch::Keysym && identity.key == keysyms::KEY_Escape) || (identity.match_mode == BindingMatch::Keysym
&& identity.key == keysyms::KEY_Escape)
|| (identity.match_mode == BindingMatch::Physical || (identity.match_mode == BindingMatch::Physical
&& (0..keymap.num_layouts_for_key(Keycode::new(identity.key))).any(|layout| { && (0..keymap.num_layouts_for_key(Keycode::new(identity.key))).any(
(0..keymap.num_levels_for_key(Keycode::new(identity.key), layout)).any(|level| { |layout| {
keymap (0..keymap.num_levels_for_key(Keycode::new(identity.key), layout))
.key_get_syms_by_level(Keycode::new(identity.key), layout, level) .any(|level| {
.iter() keymap
.any(|symbol| symbol.raw() == keysyms::KEY_Escape) .key_get_syms_by_level(
}) Keycode::new(identity.key),
})) layout,
level,
)
.iter()
.any(|symbol| symbol.raw() == keysyms::KEY_Escape)
})
},
))
{ {
return Err(ConfigError::Binding(format!( return Err(ConfigError::Binding(format!(
"invalid, duplicate or reserved mode binding {:?}", "invalid, duplicate or reserved mode binding {:?}",
@@ -95,12 +103,17 @@ impl Config {
} }
modes.maps.insert(mode.name.clone(), bindings.into()); modes.maps.insert(mode.name.clone(), bindings.into());
} }
for binding in default.iter().chain(modes.maps.values().flat_map(|map| map.iter())) { for binding in default
.iter()
.chain(modes.maps.values().flat_map(|map| map.iter()))
{
if let BindingAction::BindingMode(name) = &binding.action if let BindingAction::BindingMode(name) = &binding.action
&& name != "default" && name != "default"
&& !modes.maps.contains_key(name) && !modes.maps.contains_key(name)
{ {
return Err(ConfigError::Binding(format!("unknown binding mode {name:?}"))); return Err(ConfigError::Binding(format!(
"unknown binding mode {name:?}"
)));
} }
} }
Ok(modes) Ok(modes)
@@ -131,7 +144,14 @@ mod tests {
assert!(modes.set("resize")); assert!(modes.set("resize"));
let selected = modes.bindings(&default); let selected = modes.bindings(&default);
assert_eq!( assert_eq!(
selected.action_for_key(Keycode::new(43), &[keysyms::KEY_h], false, false, false, false), selected.action_for_key(
Keycode::new(43),
&[keysyms::KEY_h],
false,
false,
false,
false
),
Some(&BindingAction::Resize(Direction::Left)) Some(&BindingAction::Resize(Direction::Left))
); );
assert_eq!( assert_eq!(
@@ -139,11 +159,25 @@ mod tests {
Some(&BindingAction::Resize(Direction::Up)) Some(&BindingAction::Resize(Direction::Up))
); );
assert_eq!( assert_eq!(
selected.action_for_key(Keycode::new(43), &[keysyms::KEY_h], true, false, false, false), selected.action_for_key(
Keycode::new(43),
&[keysyms::KEY_h],
true,
false,
false,
false
),
None None
); );
assert_eq!( assert_eq!(
selected.action_for_key(Keycode::new(36), &[keysyms::KEY_Return], true, false, false, false), selected.action_for_key(
Keycode::new(36),
&[keysyms::KEY_Return],
true,
false,
false,
false
),
None None
); );
assert!(!modes.set("missing")); assert!(!modes.set("missing"));
+8 -3
View File
@@ -57,11 +57,15 @@ impl Config {
} }
pub fn spring_config(&self) -> Result<SpringConfig, ConfigError> { pub fn spring_config(&self) -> Result<SpringConfig, ConfigError> {
self.animations.spring_config().map_err(ConfigError::Binding) self.animations
.spring_config()
.map_err(ConfigError::Binding)
} }
pub fn viewport_spring_config(&self) -> Result<SpringConfig, ConfigError> { pub fn viewport_spring_config(&self) -> Result<SpringConfig, ConfigError> {
self.animations.viewport_config().map_err(ConfigError::Binding) self.animations
.viewport_config()
.map_err(ConfigError::Binding)
} }
} }
@@ -156,7 +160,8 @@ mod tests {
#[test] #[test]
fn compositor_speed_uses_shared_motion_bounds() { fn compositor_speed_uses_shared_motion_bounds() {
for speed in [1e-100, 0.009, 101.0, 1e100] { for speed in [1e-100, 0.009, 101.0, 1e100] {
let config = Config::parse_source(&format!("animations {{ speed {speed:e}; }}")).unwrap(); let config =
Config::parse_source(&format!("animations {{ speed {speed:e}; }}")).unwrap();
assert!(config.animation_speed().is_err()); assert!(config.animation_speed().is_err());
} }
} }
+19 -5
View File
@@ -58,7 +58,11 @@ impl Config {
} }
pub fn scrolling_focus_strategy(&self) -> ViewportFocusStrategy { 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::Minimal => ViewportFocusStrategy::Minimal,
FocusStrategyValue::CenterOnFocus => ViewportFocusStrategy::Center, FocusStrategyValue::CenterOnFocus => ViewportFocusStrategy::Center,
FocusStrategyValue::Paged => ViewportFocusStrategy::Paged, FocusStrategyValue::Paged => ViewportFocusStrategy::Paged,
@@ -66,7 +70,10 @@ impl Config {
} }
} }
pub(super) fn nonnegative_layout_value(value: f64, field: &'static str) -> Result<f64, ConfigError> { pub(super) fn nonnegative_layout_value(
value: f64,
field: &'static str,
) -> Result<f64, ConfigError> {
if value.is_finite() && value >= 0.0 { if value.is_finite() && value >= 0.0 {
Ok(value) Ok(value)
} else { } else {
@@ -74,10 +81,17 @@ pub(super) fn nonnegative_layout_value(value: f64, field: &'static str) -> Resul
} }
} }
pub(super) fn parse_column_width(value: &ColumnWidthValue, field: &'static str) -> Result<ColumnWidth, ConfigError> { pub(super) fn parse_column_width(
value: &ColumnWidthValue,
field: &'static str,
) -> Result<ColumnWidth, ConfigError> {
match value { match value {
ColumnWidthValue::Proportion(value) if value.is_finite() && *value > 0.0 => Ok(ColumnWidth::Proportion(*value)), ColumnWidthValue::Proportion(value) if value.is_finite() && *value > 0.0 => {
ColumnWidthValue::Named(value) if value.eq_ignore_ascii_case("full") => Ok(ColumnWidth::Full), Ok(ColumnWidth::Proportion(*value))
}
ColumnWidthValue::Named(value) if value.eq_ignore_ascii_case("full") => {
Ok(ColumnWidth::Full)
}
value => Err(ConfigError::ColumnWidth { value => Err(ConfigError::ColumnWidth {
field, field,
value: match value { value: match value {
+12 -4
View File
@@ -20,7 +20,10 @@ pub(crate) fn read_source(path: &Path) -> Result<String, String> {
Ok(source) Ok(source)
} }
pub(crate) fn prepare(source: &str, directory: &Path) -> Result<(Config, RuntimeConfig, Candidate), String> { pub(crate) fn prepare(
source: &str,
directory: &Path,
) -> Result<(Config, RuntimeConfig, Candidate), String> {
let document = Document::parse(source).map_err(|e| e.to_string())?; let document = Document::parse(source).map_err(|e| e.to_string())?;
prepare_document(&document, directory) prepare_document(&document, directory)
} }
@@ -68,9 +71,11 @@ pub(crate) fn prepare_document(
) -> Result<(Config, RuntimeConfig, Candidate), String> { ) -> Result<(Config, RuntimeConfig, Candidate), String> {
let candidate = crate::theme::resolve_candidate(document, directory)?; let candidate = crate::theme::resolve_candidate(document, directory)?;
let config: Config = let config: Config =
serde_json::from_value(document.with_theme(&candidate.theme).value().clone()).map_err(|e| e.to_string())?; serde_json::from_value(document.with_theme(&candidate.theme).value().clone())
.map_err(|e| e.to_string())?;
let mut runtime = config.runtime_config().map_err(|e| e.to_string())?; let mut runtime = config.runtime_config().map_err(|e| e.to_string())?;
runtime.theme_settings = Config::resolved_theme_settings(&candidate.theme).map_err(|e| e.to_string())?; runtime.theme_settings =
Config::resolved_theme_settings(&candidate.theme).map_err(|e| e.to_string())?;
Ok((config, runtime, candidate)) Ok((config, runtime, candidate))
} }
@@ -119,7 +124,10 @@ mod tests {
let valid = startup(Some(&path)).unwrap(); let valid = startup(Some(&path)).unwrap();
assert!(valid.error.is_none()); assert!(valid.error.is_none());
assert_eq!(valid.source.as_deref(), Some(source)); assert_eq!(valid.source.as_deref(), Some(source));
assert_eq!(valid.runtime.panel_style, ferese_panel::PanelStyle::Floating); assert_eq!(
valid.runtime.panel_style,
ferese_panel::PanelStyle::Floating
);
assert_eq!( assert_eq!(
Document::parse(valid.source.as_deref().unwrap()) Document::parse(valid.source.as_deref().unwrap())
.unwrap() .unwrap()
+17 -6
View File
@@ -9,13 +9,19 @@ impl Config {
.map(|profile| { .map(|profile| {
let name = profile.name.trim(); let name = profile.name.trim();
if name.is_empty() { if name.is_empty() {
return Err(ConfigError::OutputProfile("profile names cannot be empty".to_owned())); return Err(ConfigError::OutputProfile(
"profile names cannot be empty".to_owned(),
));
} }
if !names.insert(name.to_owned()) { if !names.insert(name.to_owned()) {
return Err(ConfigError::OutputProfile(format!("duplicate profile name {name:?}"))); return Err(ConfigError::OutputProfile(format!(
"duplicate profile name {name:?}"
)));
} }
if profile.outputs.is_empty() { if profile.outputs.is_empty() {
return Err(ConfigError::OutputProfile(format!("profile {name:?} has no outputs"))); return Err(ConfigError::OutputProfile(format!(
"profile {name:?} has no outputs"
)));
} }
let mut matchers = HashSet::new(); let mut matchers = HashSet::new();
@@ -70,13 +76,16 @@ impl Config {
))); )));
} }
if let Some(source) = &profile.mirror_source if let Some(source) = &profile.mirror_source
&& !outputs.iter().any(|output| &output.matcher == source && output.enabled) && !outputs
.iter()
.any(|output| &output.matcher == source && output.enabled)
{ {
return Err(ConfigError::OutputProfile(format!( return Err(ConfigError::OutputProfile(format!(
"profile {name:?}: mirror source must name an enabled output" "profile {name:?}: mirror source must name an enabled output"
))); )));
} }
if profile.layout == OutputLayout::Mirror && outputs.iter().filter(|output| output.enabled).count() < 2 if profile.layout == OutputLayout::Mirror
&& outputs.iter().filter(|output| output.enabled).count() < 2
{ {
return Err(ConfigError::OutputProfile(format!( return Err(ConfigError::OutputProfile(format!(
"profile {name:?}: mirror needs at least two enabled outputs" "profile {name:?}: mirror needs at least two enabled outputs"
@@ -101,7 +110,9 @@ pub(super) fn parse_output_mode(value: &str) -> Result<OutputModeRequest, String
let (size, refresh) = value let (size, refresh) = value
.trim() .trim()
.split_once('@') .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 let (width, height) = size
.split_once('x') .split_once('x')
.ok_or_else(|| format!("mode {value:?} must use WIDTHxHEIGHT or WIDTHxHEIGHT@REFRESH"))?; .ok_or_else(|| format!("mode {value:?} must use WIDTHxHEIGHT or WIDTHxHEIGHT@REFRESH"))?;
+36 -8
View File
@@ -74,8 +74,12 @@ impl Config {
Err(ConfigError::LayoutValue { field, value }) Err(ConfigError::LayoutValue { field, value })
} }
}; };
let color = let color = |value: &Option<String>, field| {
|value: &Option<String>, field| value.as_ref().map(|s| parse_color(s, field).map(|c| c.0)).transpose(); value
.as_ref()
.map(|s| parse_color(s, field).map(|c| c.0))
.transpose()
};
let width = range(c.width, 1.0, 16.0, "tab_indicator.width")?; let width = range(c.width, 1.0, 16.0, "tab_indicator.width")?;
let hit_width = range(c.hit_width, width, 64.0, "tab_indicator.hit_width")?; let hit_width = range(c.hit_width, width, 64.0, "tab_indicator.hit_width")?;
let font_size = range(c.font_size, 8.0, 32.0, "tab_indicator.font_size")?; let font_size = range(c.font_size, 8.0, 32.0, "tab_indicator.font_size")?;
@@ -85,11 +89,21 @@ impl Config {
font_size, font_size,
outline_width: range(c.outline_width, 0.0, 8.0, "tab_indicator.outline_width")?, outline_width: range(c.outline_width, 0.0, 8.0, "tab_indicator.outline_width")?,
outline_color: color(&c.outline_color, "tab_indicator.outline_color")?, outline_color: color(&c.outline_color, "tab_indicator.outline_color")?,
segment_height: range(c.segment_height, width, 128.0, "tab_indicator.segment_height")?, segment_height: range(
c.segment_height,
width,
128.0,
"tab_indicator.segment_height",
)?,
segment_gap: range(c.segment_gap, 0.0, 64.0, "tab_indicator.segment_gap")?, segment_gap: range(c.segment_gap, 0.0, 64.0, "tab_indicator.segment_gap")?,
gap_threshold: range(c.gap_threshold, width, 128.0, "tab_indicator.gap_threshold")?, gap_threshold: range(c.gap_threshold, width, 128.0, "tab_indicator.gap_threshold")?,
list_width: range(c.list_width, 80.0, 640.0, "tab_indicator.list_width")?, list_width: range(c.list_width, 80.0, 640.0, "tab_indicator.list_width")?,
row_height: range(c.row_height, font_size + 6.0, 80.0, "tab_indicator.row_height")?, row_height: range(
c.row_height,
font_size + 6.0,
80.0,
"tab_indicator.row_height",
)?,
padding: range(c.padding, 0.0, 24.0, "tab_indicator.padding")?, padding: range(c.padding, 0.0, 24.0, "tab_indicator.padding")?,
flash_duration: std::time::Duration::from_millis(range( flash_duration: std::time::Duration::from_millis(range(
c.flash_duration_ms as f64, c.flash_duration_ms as f64,
@@ -111,9 +125,18 @@ mod tests {
#[test] #[test]
fn indicator_defaults_overrides_and_validation() { fn indicator_defaults_overrides_and_validation() {
let c = Config::parse_source("").unwrap().tab_indicator_settings().unwrap(); let c = Config::parse_source("")
.unwrap()
.tab_indicator_settings()
.unwrap();
assert_eq!( assert_eq!(
(c.width, c.segment_height, c.segment_gap, c.hit_width, c.gap_threshold), (
c.width,
c.segment_height,
c.segment_gap,
c.hit_width,
c.gap_threshold
),
(4., 24., 4., 12., 8.) (4., 24., 4., 12., 8.)
); );
assert_eq!(c.flash_duration.as_millis(), 1000); assert_eq!(c.flash_duration.as_millis(), 1000);
@@ -123,7 +146,10 @@ mod tests {
) )
.unwrap(); .unwrap();
let c = config.tab_indicator_settings().unwrap(); let c = config.tab_indicator_settings().unwrap();
assert_eq!((c.width, c.hit_width, c.flash_duration.as_millis()), (3., 14., 750)); assert_eq!(
(c.width, c.hit_width, c.flash_duration.as_millis()),
(3., 14., 750)
);
assert_eq!(c.active_color, Some([1., 0., 0., 1.])); assert_eq!(c.active_color, Some([1., 0., 0., 1.]));
for source in [ for source in [
"width 0", "width 0",
@@ -133,7 +159,9 @@ mod tests {
"flash-duration-ms 60001", "flash-duration-ms 60001",
"active-color \"invalid\"", "active-color \"invalid\"",
] { ] {
let config = Config::parse_source(&format!("scrolling {{ tab-indicator {{ {source}; }}; }}")).unwrap(); let config =
Config::parse_source(&format!("scrolling {{ tab-indicator {{ {source}; }}; }}"))
.unwrap();
assert!(config.tab_indicator_settings().is_err(), "{source}"); assert!(config.tab_indicator_settings().is_err(), "{source}");
} }
assert!(Config::parse_source("scrolling { tab-indicator { typo 1; }; }").is_err()); assert!(Config::parse_source("scrolling { tab-indicator { typo 1; }; }").is_err());
+79 -26
View File
@@ -42,7 +42,11 @@ impl CursorFrame {
) )
} }
pub(crate) fn physical_location(&self, pointer: Point<f64, Logical>, scale: f64) -> Point<f64, Physical> { pub(crate) fn physical_location(
&self,
pointer: Point<f64, Logical>,
scale: f64,
) -> Point<f64, Physical> {
self.logical_rect(pointer).loc.to_physical(scale) self.logical_rect(pointer).loc.to_physical(scale)
} }
} }
@@ -64,7 +68,10 @@ impl NamedCursor {
let mut offset = elapsed.as_nanos() % period.as_nanos(); let mut offset = elapsed.as_nanos() % period.as_nanos();
for (index, frame) in self.frames.iter().enumerate() { for (index, frame) in self.frames.iter().enumerate() {
if offset < frame.delay.as_nanos() { if offset < frame.delay.as_nanos() {
return (index, Some(frame.delay - Duration::from_nanos(offset as u64))); return (
index,
Some(frame.delay - Duration::from_nanos(offset as u64)),
);
} }
offset -= frame.delay.as_nanos(); offset -= frame.delay.as_nanos();
} }
@@ -97,7 +104,9 @@ impl Loader {
let mut theme = None; let mut theme = None;
move |(icon, scale)| { move |(icon, scale)| {
let theme = theme.get_or_insert_with(|| { let theme = theme.get_or_insert_with(|| {
CursorTheme::load(&std::env::var("XCURSOR_THEME").unwrap_or_else(|_| "default".into())) CursorTheme::load(
&std::env::var("XCURSOR_THEME").unwrap_or_else(|_| "default".into()),
)
}); });
load_named_cursor(theme, icon, scale) load_named_cursor(theme, icon, scale)
} }
@@ -135,7 +144,14 @@ impl Loader {
drop(requests); drop(requests);
let cursor = load(request); let cursor = load(request);
let (icon, scale) = request; let (icon, scale) = request;
if sender.send(Loaded { icon, scale, cursor }).is_err() { if sender
.send(Loaded {
icon,
scale,
cursor,
})
.is_err()
{
break; break;
} }
signal.wakeup(); signal.wakeup();
@@ -218,7 +234,12 @@ impl crate::Ferese {
} }
.unwrap_or(1) .unwrap_or(1)
.clamp(1, 8); .clamp(1, 8);
while let Ok(Loaded { icon, scale, cursor }) = self.cursor_loader.results.try_recv() { while let Ok(Loaded {
icon,
scale,
cursor,
}) = self.cursor_loader.results.try_recv()
{
let mut cursor = cursor.unwrap_or_else(fallback_cursor); let mut cursor = cursor.unwrap_or_else(fallback_cursor);
cursor.requested_scale = scale; cursor.requested_scale = scale;
let active = match self.cursor_status { let active = match self.cursor_status {
@@ -230,12 +251,9 @@ impl crate::Ferese {
&& active.is_some_and(|active| !self.named_cursors.contains_key(&active))); && active.is_some_and(|active| !self.named_cursors.contains_key(&active)));
let mut bytes: usize = self.named_cursors.values().map(|cursor| cursor.bytes).sum(); let mut bytes: usize = self.named_cursors.values().map(|cursor| cursor.bytes).sum();
while bytes + cursor.bytes > MAX_CACHE_BYTES { while bytes + cursor.bytes > MAX_CACHE_BYTES {
let Some(evict) = self let Some(evict) = self.named_cursors.keys().copied().find(|key| {
.named_cursors *key != CursorIcon::Default && *key != icon && Some(*key) != active
.keys() }) else {
.copied()
.find(|key| *key != CursorIcon::Default && *key != icon && Some(*key) != active)
else {
break; break;
}; };
bytes -= self.named_cursors.remove(&evict).unwrap().bytes; bytes -= self.named_cursors.remove(&evict).unwrap().bytes;
@@ -243,7 +261,8 @@ impl crate::Ferese {
self.named_cursors.insert(icon, cursor); self.named_cursors.insert(icon, cursor);
if affects_current { if affects_current {
self.cursor_animation.reset = true; self.cursor_animation.reset = true;
self.cursor_redraw_pending |= crate::backends::direct::cursor_animation_visible(self); self.cursor_redraw_pending |=
crate::backends::direct::cursor_animation_visible(self);
} }
} }
@@ -257,7 +276,8 @@ impl crate::Ferese {
.get(&icon) .get(&icon)
.is_none_or(|cursor| cursor.requested_scale != self.cursor_animation.buffer_scale) .is_none_or(|cursor| cursor.requested_scale != self.cursor_animation.buffer_scale)
{ {
self.cursor_loader.request(icon, self.cursor_animation.buffer_scale); self.cursor_loader
.request(icon, self.cursor_animation.buffer_scale);
} }
let animated = icon let animated = icon
.and_then(|icon| { .and_then(|icon| {
@@ -285,7 +305,8 @@ impl crate::Ferese {
.get(&icon) .get(&icon)
.or_else(|| self.named_cursors.get(&CursorIcon::Default)) .or_else(|| self.named_cursors.get(&CursorIcon::Default))
.unwrap(); .unwrap();
let (frame, remaining) = cursor.sample(now.saturating_duration_since(self.cursor_animation.epoch)); let (frame, remaining) =
cursor.sample(now.saturating_duration_since(self.cursor_animation.epoch));
if self.cursor_animation.frame != frame { if self.cursor_animation.frame != frame {
self.cursor_animation.frame = frame; self.cursor_animation.frame = frame;
self.cursor_redraw_pending = true; self.cursor_redraw_pending = true;
@@ -293,13 +314,14 @@ impl crate::Ferese {
if let Some(remaining) = remaining if let Some(remaining) = remaining
&& self.cursor_animation.timer.is_none() && self.cursor_animation.timer.is_none()
{ {
match self match self.loop_handle.insert_source(
.loop_handle Timer::from_deadline(now + remaining),
.insert_source(Timer::from_deadline(now + remaining), |_, _, state| { |_, _, state| {
state.cursor_animation.timer = None; state.cursor_animation.timer = None;
state.cursor_redraw_pending = true; state.cursor_redraw_pending = true;
TimeoutAction::Drop TimeoutAction::Drop
}) { },
) {
Ok(token) => self.cursor_animation.timer = Some(token), Ok(token) => self.cursor_animation.timer = Some(token),
Err(error) => tracing::warn!(%error, "could not schedule animated cursor"), Err(error) => tracing::warn!(%error, "could not schedule animated cursor"),
} }
@@ -321,7 +343,9 @@ fn load_named_cursor(theme: &CursorTheme, icon: CursorIcon, scale: i32) -> Optio
return None; return None;
} }
let mut bytes = Vec::new(); let mut bytes = Vec::new();
file.take((MAX_FILE_BYTES + 1) as u64).read_to_end(&mut bytes).ok()?; file.take((MAX_FILE_BYTES + 1) as u64)
.read_to_end(&mut bytes)
.ok()?;
parse_cursor(&bytes, requested, scale) parse_cursor(&bytes, requested, scale)
}) })
} }
@@ -348,7 +372,10 @@ fn parse_cursor(bytes: &[u8], requested: u32, scale: i32) -> Option<NamedCursor>
} }
let header = word(bytes, 4)? as usize; let header = word(bytes, 4)? as usize;
let count = word(bytes, 12)? as usize; let count = word(bytes, 12)? as usize;
if header < 16 || count > MAX_FRAMES || header.checked_add(count.checked_mul(12)?)? > bytes.len() { if header < 16
|| count > MAX_FRAMES
|| header.checked_add(count.checked_mul(12)?)? > bytes.len()
{
return None; return None;
} }
let mut images = Vec::new(); let mut images = Vec::new();
@@ -371,7 +398,14 @@ fn parse_cursor(bytes: &[u8], requested: u32, scale: i32) -> Option<NamedCursor>
let height = word(bytes, pos + 20)?; let height = word(bytes, pos + 20)?;
let xhot = word(bytes, pos + 24)?; let xhot = word(bytes, pos + 24)?;
let yhot = word(bytes, pos + 28)?; let yhot = word(bytes, pos + 28)?;
if nominal == 0 || width == 0 || height == 0 || width > 1024 || height > 1024 || xhot > width || yhot > height { if nominal == 0
|| width == 0
|| height == 0
|| width > 1024
|| height > 1024
|| xhot > width
|| yhot > height
{
return None; return None;
} }
let pixels = (width as usize).checked_mul(height as usize)?; let pixels = (width as usize).checked_mul(height as usize)?;
@@ -382,7 +416,15 @@ fn parse_cursor(bytes: &[u8], requested: u32, scale: i32) -> Option<NamedCursor>
let start = pos.checked_add(36)?; let start = pos.checked_add(36)?;
let end = start.checked_add(pixels.checked_mul(4)?)?; let end = start.checked_add(pixels.checked_mul(4)?)?;
let rgba = bytes.get(start..end)?; let rgba = bytes.get(start..end)?;
images.push((nominal, width, height, xhot, yhot, word(bytes, pos + 32)?, rgba)); images.push((
nominal,
width,
height,
xhot,
yhot,
word(bytes, pos + 32)?,
rgba,
));
} }
let base = images let base = images
.iter() .iter()
@@ -396,7 +438,9 @@ fn parse_cursor(bytes: &[u8], requested: u32, scale: i32) -> Option<NamedCursor>
.min_by_key(|size| (size.abs_diff(desired), *size))?; .min_by_key(|size| (size.abs_diff(desired), *size))?;
let mut frames = Vec::new(); let mut frames = Vec::new();
let mut asset_bytes = 0usize; let mut asset_bytes = 0usize;
for (_, width, height, xhot, yhot, delay, rgba) in images.into_iter().filter(|image| image.0 == nominal) { for (_, width, height, xhot, yhot, delay, rgba) in
images.into_iter().filter(|image| image.0 == nominal)
{
let ratio = f64::from(desired) / f64::from(nominal); let ratio = f64::from(desired) / f64::from(nominal);
let w = (f64::from(width) * ratio).round().max(1.0) as u32; let w = (f64::from(width) * ratio).round().max(1.0) as u32;
let h = (f64::from(height) * ratio).round().max(1.0) as u32; let h = (f64::from(height) * ratio).round().max(1.0) as u32;
@@ -404,20 +448,29 @@ fn parse_cursor(bytes: &[u8], requested: u32, scale: i32) -> Option<NamedCursor>
return None; return None;
} }
let size: Size<i32, Buffer> = (w as i32, h as i32).into(); let size: Size<i32, Buffer> = (w as i32, h as i32).into();
asset_bytes = asset_bytes.checked_add((w as usize).checked_mul(h as usize)?.checked_mul(4)?)?; asset_bytes =
asset_bytes.checked_add((w as usize).checked_mul(h as usize)?.checked_mul(4)?)?;
if asset_bytes > MAX_PIXELS * 4 { if asset_bytes > MAX_PIXELS * 4 {
return None; return None;
} }
let resized; let resized;
let pixels = if w != width || h != height { let pixels = if w != width || h != height {
let original = image::RgbaImage::from_raw(width, height, rgba.to_vec())?; let original = image::RgbaImage::from_raw(width, height, rgba.to_vec())?;
resized = image::imageops::resize(&original, w, h, image::imageops::FilterType::Triangle); resized =
image::imageops::resize(&original, w, h, image::imageops::FilterType::Triangle);
resized.as_raw().as_slice() resized.as_raw().as_slice()
} else { } else {
rgba rgba
}; };
frames.push(CursorFrame { frames.push(CursorFrame {
buffer: MemoryRenderBuffer::from_slice(pixels, Fourcc::Abgr8888, size, scale, Transform::Normal, None), buffer: MemoryRenderBuffer::from_slice(
pixels,
Fourcc::Abgr8888,
size,
scale,
Transform::Normal,
None,
),
hotspot: ( hotspot: (
(f64::from(xhot) * ratio).round() as i32, (f64::from(xhot) * ratio).round() as i32,
(f64::from(yhot) * ratio).round() as i32, (f64::from(yhot) * ratio).round() as i32,
+56 -15
View File
@@ -7,17 +7,27 @@ use smithay::utils::SERIAL_COUNTER;
fn file(frames: &[(u32, u32, u32, u32)]) -> Vec<u8> { fn file(frames: &[(u32, u32, u32, u32)]) -> Vec<u8> {
let mut bytes = b"Xcur".to_vec(); let mut bytes = b"Xcur".to_vec();
bytes.extend([16u32, 1, frames.len() as u32].into_iter().flat_map(u32::to_le_bytes)); bytes.extend(
[16u32, 1, frames.len() as u32]
.into_iter()
.flat_map(u32::to_le_bytes),
);
let mut pos = 16 + frames.len() * 12; let mut pos = 16 + frames.len() * 12;
for &(size, width, _, _) in frames { for &(size, width, _, _) in frames {
bytes.extend([0xfffd0002, size, pos as u32].into_iter().flat_map(u32::to_le_bytes)); bytes.extend(
[0xfffd0002, size, pos as u32]
.into_iter()
.flat_map(u32::to_le_bytes),
);
pos += 36 + width as usize * width as usize * 4; pos += 36 + width as usize * width as usize * 4;
} }
for &(size, width, hotspot, delay) in frames { for &(size, width, hotspot, delay) in frames {
bytes.extend( bytes.extend(
[36, 0xfffd0002, size, 1, width, width, hotspot, hotspot, delay] [
.into_iter() 36, 0xfffd0002, size, 1, width, width, hotspot, hotspot, delay,
.flat_map(u32::to_le_bytes), ]
.into_iter()
.flat_map(u32::to_le_bytes),
); );
bytes.extend([255, 255, 255, 255].repeat(width as usize * width as usize)); bytes.extend([255, 255, 255, 255].repeat(width as usize * width as usize));
} }
@@ -49,12 +59,20 @@ fn unavailable_hidpi_size_preserves_logical_size_and_hotspot() {
assert_eq!(frame.buffer_scale, 2); assert_eq!(frame.buffer_scale, 2);
assert_eq!(frame.size.w, 64); assert_eq!(frame.size.w, 64);
assert_eq!(frame.hotspot.x, 32); assert_eq!(frame.hotspot.x, 32);
assert_eq!(frame.logical_rect((100.0, 100.0).into()).size, (32.0, 32.0).into()); assert_eq!(
frame.logical_rect((100.0, 100.0).into()).size,
(32.0, 32.0).into()
);
} }
#[test] #[test]
fn selects_a_nominal_sequence_and_preserves_frame_delays() { fn selects_a_nominal_sequence_and_preserves_frame_delays() {
let asset = parse_cursor(&file(&[(48, 48, 24, 70), (24, 24, 12, 30), (24, 24, 12, 80)]), 24, 1).unwrap(); let asset = parse_cursor(
&file(&[(48, 48, 24, 70), (24, 24, 12, 30), (24, 24, 12, 80)]),
24,
1,
)
.unwrap();
assert_eq!(asset.frames.len(), 2); assert_eq!(asset.frames.len(), 2);
for (time, index, remaining) in [ for (time, index, remaining) in [
(0, 0, 30), (0, 0, 30),
@@ -93,7 +111,11 @@ fn rejects_oversized_and_malformed_assets_before_buffer_allocation() {
bytes.truncate(16); bytes.truncate(16);
bytes[12..16].copy_from_slice(&3u32.to_le_bytes()); bytes[12..16].copy_from_slice(&3u32.to_le_bytes());
for _ in 0..3 { for _ in 0..3 {
bytes.extend([0xfffd0002, 1024, 52].into_iter().flat_map(u32::to_le_bytes)); bytes.extend(
[0xfffd0002, 1024, 52]
.into_iter()
.flat_map(u32::to_le_bytes),
);
} }
bytes.extend(frame); bytes.extend(frame);
assert!(parse_cursor(&bytes, 24, 1).is_none()); assert!(parse_cursor(&bytes, 24, 1).is_none());
@@ -143,9 +165,15 @@ fn delayed_loading_does_not_block_cursor_callbacks_or_pointer_dispatch() {
state.named_cursor_frame().is_some(), state.named_cursor_frame().is_some(),
"cached fallback must remain visible" "cached fallback must remain visible"
); );
assert!(calls.try_recv().is_err(), "decoder is still deliberately blocked"); assert!(
calls.try_recv().is_err(),
"decoder is still deliberately blocked"
);
release.send(()).unwrap(); release.send(()).unwrap();
assert_eq!(calls.recv_timeout(Duration::from_secs(2)).unwrap().0, CursorIcon::Text); assert_eq!(
calls.recv_timeout(Duration::from_secs(2)).unwrap().0,
CursorIcon::Text
);
} }
#[test] #[test]
@@ -188,7 +216,9 @@ fn cursor_animation_advances_and_cancels_on_hide_and_shape_change() {
assert!(state.cursor_animation.timer.is_some()); assert!(state.cursor_animation.timer.is_some());
// Invoke the same deadline callback by dispatching the real timer. // Invoke the same deadline callback by dispatching the real timer.
while Instant::now() < now + Duration::from_millis(35) { while Instant::now() < now + Duration::from_millis(35) {
events.dispatch(Duration::from_millis(5), &mut state).unwrap(); events
.dispatch(Duration::from_millis(5), &mut state)
.unwrap();
} }
state.update_named_cursor(now + Duration::from_millis(30)); state.update_named_cursor(now + Duration::from_millis(30));
assert_eq!(state.cursor_animation.frame, 1); assert_eq!(state.cursor_animation.frame, 1);
@@ -200,7 +230,9 @@ fn cursor_animation_advances_and_cancels_on_hide_and_shape_change() {
state.cursor_image(&seat, CursorImageStatus::Named(CursorIcon::Wait)); state.cursor_image(&seat, CursorImageStatus::Named(CursorIcon::Wait));
state.update_named_cursor(now + Duration::from_millis(32)); state.update_named_cursor(now + Duration::from_millis(32));
assert!(state.cursor_animation.timer.is_some()); assert!(state.cursor_animation.timer.is_some());
state.named_cursors.insert(CursorIcon::Text, fallback_cursor()); state
.named_cursors
.insert(CursorIcon::Text, fallback_cursor());
state.cursor_image(&seat, CursorImageStatus::Named(CursorIcon::Text)); state.cursor_image(&seat, CursorImageStatus::Named(CursorIcon::Text));
state.update_named_cursor(now + Duration::from_millis(33)); state.update_named_cursor(now + Duration::from_millis(33));
assert!(state.cursor_animation.timer.is_none()); assert!(state.cursor_animation.timer.is_none());
@@ -208,9 +240,18 @@ fn cursor_animation_advances_and_cancels_on_hide_and_shape_change() {
#[test] #[test]
fn supplies_legacy_fallback_names_for_common_cursors() { fn supplies_legacy_fallback_names_for_common_cursors() {
assert_eq!(cursor_names(CursorIcon::Default), ["default", "left_ptr", "arrow"]); assert_eq!(
assert_eq!(cursor_names(CursorIcon::Pointer), ["pointer", "hand2", "left_ptr"]); cursor_names(CursorIcon::Default),
assert_eq!(cursor_names(CursorIcon::Text), ["text", "xterm", "left_ptr"]); ["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!( assert_eq!(
cursor_names(CursorIcon::Crosshair), cursor_names(CursorIcon::Crosshair),
["crosshair", "default", "left_ptr"] ["crosshair", "default", "left_ptr"]
+36 -14
View File
@@ -98,15 +98,16 @@ impl Runner {
} }
if let Some(deadline) = deadline { if let Some(deadline) = deadline {
let weak = Rc::downgrade(owner); let weak = Rc::downgrade(owner);
match state match state.loop_handle.insert_source(
.loop_handle Timer::from_deadline(deadline),
.insert_source(Timer::from_deadline(deadline), move |_, _, state| { move |_, _, state| {
if let Some(owner) = weak.upgrade() { if let Some(owner) = weak.upgrade() {
owner.borrow_mut().2 = None; owner.borrow_mut().2 = None;
Self::refresh(&owner, state); Self::refresh(&owner, state);
} }
TimeoutAction::Drop TimeoutAction::Drop
}) { },
) {
Ok(token) => runner.2 = Some((token, deadline)), Ok(token) => runner.2 = Some((token, deadline)),
Err(error) => tracing::warn!(%error, "could not arm daemon restart deadline"), Err(error) => tracing::warn!(%error, "could not arm daemon restart deadline"),
} }
@@ -119,7 +120,11 @@ impl Runner {
.iter() .iter()
.filter(|c| c.enabled && policy.allows_autostart(c.nested)) .filter(|c| c.enabled && policy.allows_autostart(c.nested))
{ {
if self.0.iter().any(|service| service.config.command == config.command) { if self
.0
.iter()
.any(|service| service.config.command == config.command)
{
continue; continue;
} }
if let Some(index) = previous if let Some(index) = previous
@@ -187,7 +192,11 @@ impl Runner {
} }
} }
if !service.finished && now >= service.next_start { if !service.finished && now >= service.next_start {
service.child = spawn_client(state, service.config.command.iter(), ClientCapabilities::default()); service.child = spawn_client(
state,
service.config.command.iter(),
ClientCapabilities::default(),
);
service.next_start = now + Duration::from_secs(5); service.next_start = now + Duration::from_secs(5);
if service.child.is_none() && !service.config.restart { if service.child.is_none() && !service.config.restart {
service.finished = true; service.finished = true;
@@ -225,14 +234,19 @@ mod tests {
let mut events = smithay::reexports::calloop::EventLoop::<usize>::try_new().unwrap(); let mut events = smithay::reexports::calloop::EventLoop::<usize>::try_new().unwrap();
events events
.handle() .handle()
.insert_source(Generic::new(fd, Interest::READ, Mode::Level), move |_, _, count| { .insert_source(
assert_eq!(child.wait().unwrap().code(), Some(7)); Generic::new(fd, Interest::READ, Mode::Level),
*count += 1; move |_, _, count| {
Ok(PostAction::Remove) assert_eq!(child.wait().unwrap().code(), Some(7));
}) *count += 1;
Ok(PostAction::Remove)
},
)
.unwrap(); .unwrap();
let mut count = 0; let mut count = 0;
events.dispatch(Some(Duration::from_secs(2)), &mut count).unwrap(); events
.dispatch(Some(Duration::from_secs(2)), &mut count)
.unwrap();
assert_eq!(count, 1); assert_eq!(count, 1);
events.dispatch(Some(Duration::ZERO), &mut count).unwrap(); events.dispatch(Some(Duration::ZERO), &mut count).unwrap();
assert_eq!(count, 1); assert_eq!(count, 1);
@@ -247,7 +261,12 @@ mod tests {
nested: false, nested: false,
}; };
let mut runner = Runner::new(&[config.clone()], SessionPolicy::Desktop); let mut runner = Runner::new(&[config.clone()], SessionPolicy::Desktop);
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(); let pid = runner.0[0].child.as_ref().unwrap().id();
config.restart = true; config.restart = true;
runner.reconcile(&[config.clone()], SessionPolicy::Desktop); runner.reconcile(&[config.clone()], SessionPolicy::Desktop);
@@ -260,7 +279,10 @@ mod tests {
assert!(runner.0.is_empty()); assert!(runner.0.is_empty());
let deadline = Instant::now() + Duration::from_secs(2); let deadline = Instant::now() + Duration::from_secs(2);
while unsafe { libc::kill(pid as i32, 0) } == 0 { 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)); std::thread::sleep(Duration::from_millis(10));
} }
} }
+5 -2
View File
@@ -46,7 +46,9 @@ impl DisplayPresentation {
#[cfg(test)] #[cfg(test)]
pub fn visible(&self, surface: &Id) -> bool { pub fn visible(&self, surface: &Id) -> bool {
self.outputs.values().any(|record| record.displayed.contains(surface)) self.outputs
.values()
.any(|record| record.displayed.contains(surface))
} }
pub fn outputs_for<'a>(&'a self, surface: &'a Id) -> impl Iterator<Item = &'a Output> { pub fn outputs_for<'a>(&'a self, surface: &'a Id) -> impl Iterator<Item = &'a Output> {
@@ -143,7 +145,8 @@ mod tests {
states.states.get_mut(&id).unwrap().visible_area = 0; states.states.get_mut(&id).unwrap().visible_area = 0;
assert!(visible(&states).is_empty()); assert!(visible(&states).is_empty());
states.states.get_mut(&id).unwrap().visible_area = 20; states.states.get_mut(&id).unwrap().visible_area = 20;
states.states.get_mut(&id).unwrap().presentation_state = RenderElementPresentationState::Skipped; states.states.get_mut(&id).unwrap().presentation_state =
RenderElementPresentationState::Skipped;
assert!(visible(&states).is_empty()); assert!(visible(&states).is_empty());
} }
} }
+13 -3
View File
@@ -22,7 +22,11 @@ impl FeedbackManager {
self.globals.insert(global, render); self.globals.insert(global, render);
} }
pub fn new_surface(&mut self, surface: &WlSurface, global: DmabufGlobal) -> Option<DmabufFeedback> { pub fn new_surface(
&mut self,
surface: &WlSurface,
global: DmabufGlobal,
) -> Option<DmabufFeedback> {
let fallback = self.globals.get(&global)?.clone(); let fallback = self.globals.get(&global)?.clone();
self.surfaces.insert( self.surfaces.insert(
surface.clone(), surface.clone(),
@@ -35,7 +39,9 @@ impl FeedbackManager {
} }
pub fn requested(&self) -> impl Iterator<Item = (&WlSurface, DmabufGlobal)> { pub fn requested(&self) -> impl Iterator<Item = (&WlSurface, DmabufGlobal)> {
self.surfaces.iter().map(|(surface, record)| (surface, record.global)) self.surfaces
.iter()
.map(|(surface, record)| (surface, record.global))
} }
pub fn refresh(&mut self, preferred: &HashMap<WlSurface, DmabufFeedback>) { pub fn refresh(&mut self, preferred: &HashMap<WlSurface, DmabufFeedback>) {
@@ -57,7 +63,11 @@ impl FeedbackManager {
pub fn remove(&mut self, global: DmabufGlobal) { pub fn remove(&mut self, global: DmabufGlobal) {
self.globals.remove(&global); self.globals.remove(&global);
for (surface, record) in self.surfaces.iter().filter(|(_, record)| record.global == global) { for (surface, record) in self
.surfaces
.iter()
.filter(|(_, record)| record.global == global)
{
// V4 requires a main-device tranche. Retain last-known render // V4 requires a main-device tranche. Retain last-known render
// capabilities, withdraw scanout hints and retire the global. Bound // capabilities, withdraw scanout hints and retire the global. Bound
// resources cannot import through the retired global or another GPU. // resources cannot import through the retired global or another GPU.
+34 -8
View File
@@ -108,8 +108,13 @@ fn subscriptions_update_deduplicate_and_retire_device_preferences() {
if *object != 7 || *opcode != 0 { if *object != 7 || *opcode != 0 {
return None; return None;
} }
let bytes = args[2..].iter().flat_map(|word| word.to_ne_bytes()).collect::<Vec<_>>(); let bytes = args[2..]
bytes.starts_with(b"zwp_linux_dmabuf_v1\0").then_some(args[0]) .iter()
.flat_map(|word| word.to_ne_bytes())
.collect::<Vec<_>>();
bytes
.starts_with(b"zwp_linux_dmabuf_v1\0")
.then_some(args[0])
}) })
.expect("DMA-BUF v4 global"); .expect("DMA-BUF v4 global");
let name = b"zwp_linux_dmabuf_v1\0"; let name = b"zwp_linux_dmabuf_v1\0";
@@ -131,7 +136,10 @@ fn subscriptions_update_deduplicate_and_retire_device_preferences() {
assert_eq!(flags(&initial, 9), [0]); assert_eq!(flags(&initial, 9), [0]);
assert_eq!(done(&initial, 9), 1); assert_eq!(done(&initial, 9), 1);
assert_eq!(state.dmabuf_feedback.requested().count(), 1); assert_eq!(state.dmabuf_feedback.requested().count(), 1);
let main = initial.iter().find(|(id, opcode, _)| *id == 9 && *opcode == 2).unwrap(); let main = initial
.iter()
.find(|(id, opcode, _)| *id == 9 && *opcode == 2)
.unwrap();
assert_eq!(main.2[0] as usize, std::mem::size_of::<libc::dev_t>()); assert_eq!(main.2[0] as usize, std::mem::size_of::<libc::dev_t>());
assert_eq!(main.2[1], 10, "main device remains the renderer"); assert_eq!(main.2[1], 10, "main device remains the renderer");
@@ -141,11 +149,20 @@ fn subscriptions_update_deduplicate_and_retire_device_preferences() {
let changed = messages(&mut wire); let changed = messages(&mut wire);
assert_eq!(flags(&changed, 9), [TrancheFlags::Scanout.bits(), 0]); assert_eq!(flags(&changed, 9), [TrancheFlags::Scanout.bits(), 0]);
assert_eq!(done(&changed, 9), 1); assert_eq!(done(&changed, 9), 1);
let target = changed.iter().find(|(id, opcode, _)| *id == 9 && *opcode == 4).unwrap(); let target = changed
assert_eq!(target.2[1], 20, "scanout tranche targets the primary DRM device"); .iter()
.find(|(id, opcode, _)| *id == 9 && *opcode == 4)
.unwrap();
assert_eq!(
target.2[1], 20,
"scanout tranche targets the primary DRM device"
);
state.dmabuf_feedback.refresh(&selected); state.dmabuf_feedback.refresh(&selected);
state.display_handle.flush_clients().unwrap(); state.display_handle.flush_clients().unwrap();
assert!(messages(&mut wire).is_empty(), "Smithay suppresses unchanged feedback"); assert!(
messages(&mut wire).is_empty(),
"Smithay suppresses unchanged feedback"
);
request(&mut wire, 8, 3, &[10, 2], None); request(&mut wire, 8, 3, &[10, 2], None);
dispatch(&mut events, &mut state); dispatch(&mut events, &mut state);
@@ -167,7 +184,11 @@ fn subscriptions_update_deduplicate_and_retire_device_preferences() {
request(&mut wire, 8, 2, &[12], None); // get_default_feedback request(&mut wire, 8, 2, &[12], None); // get_default_feedback
dispatch(&mut events, &mut state); dispatch(&mut events, &mut state);
let renewed = messages(&mut wire); let renewed = messages(&mut wire);
assert_eq!(flags(&renewed, 11), [0], "renewal starts with safe render feedback"); assert_eq!(
flags(&renewed, 11),
[0],
"renewal starts with safe render feedback"
);
assert_eq!(flags(&renewed, 12), [0]); assert_eq!(flags(&renewed, 12), [0]);
state.dmabuf_feedback.refresh(&selected); state.dmabuf_feedback.refresh(&selected);
state.display_handle.flush_clients().unwrap(); state.display_handle.flush_clients().unwrap();
@@ -198,7 +219,12 @@ fn subscriptions_update_deduplicate_and_retire_device_preferences() {
state.dmabuf_feedback.refresh(&selected); // A stale choice cannot resurrect a retired preference. state.dmabuf_feedback.refresh(&selected); // A stale choice cannot resurrect a retired preference.
state.display_handle.flush_clients().unwrap(); state.display_handle.flush_clients().unwrap();
assert!(messages(&mut wire).is_empty()); assert!(messages(&mut wire).is_empty());
assert!(state.dmabuf_feedback.new_surface(&surface, global).is_none()); assert!(
state
.dmabuf_feedback
.new_surface(&surface, global)
.is_none()
);
drop(wire); drop(wire);
dispatch(&mut events, &mut state); dispatch(&mut events, &mut state);
state.dmabuf_feedback.refresh(&HashMap::new()); state.dmabuf_feedback.refresh(&HashMap::new());
+46 -17
View File
@@ -37,7 +37,12 @@ impl<K, C, T> Default for Queue<K, C, T> {
impl<K: Eq, C: Eq, T> Queue<K, C, T> { impl<K: Eq, C: Eq, T> Queue<K, C, T> {
fn enqueue(&mut self, owner: K, client: C, value: T, now: Instant) -> Result<(), T> { fn enqueue(&mut self, owner: K, client: C, value: T, now: Instant) -> Result<(), T> {
if self.pending.len() >= MAX_PENDING if self.pending.len() >= MAX_PENDING
|| self.pending.iter().filter(|entry| entry.client == client).count() >= MAX_PER_CLIENT || self
.pending
.iter()
.filter(|entry| entry.client == client)
.count()
>= MAX_PER_CLIENT
{ {
return Err(value); return Err(value);
} }
@@ -89,7 +94,8 @@ pub(crate) struct ImportManager {
impl Drop for ImportManager { impl Drop for ImportManager {
fn drop(&mut self) { fn drop(&mut self) {
self.queue.remove_if(|_| true, |entry| entry.value.notifier.failed()); self.queue
.remove_if(|_| true, |entry| entry.value.notifier.failed());
} }
} }
@@ -100,10 +106,17 @@ impl ImportManager {
pub(crate) fn remove(&mut self, global: DmabufGlobal) { pub(crate) fn remove(&mut self, global: DmabufGlobal) {
self.globals.remove(&global); self.globals.remove(&global);
self.queue.process(&global, |request| request.notifier.failed()); self.queue
.process(&global, |request| request.notifier.failed());
} }
pub(crate) fn enqueue(&mut self, global: DmabufGlobal, buffer: Dmabuf, notifier: ImportNotifier, now: Instant) { pub(crate) fn enqueue(
&mut self,
global: DmabufGlobal,
buffer: Dmabuf,
notifier: ImportNotifier,
now: Instant,
) {
let Some(client) = notifier.client() else { let Some(client) = notifier.client() else {
notifier.failed(); notifier.failed();
return; return;
@@ -114,16 +127,23 @@ impl ImportManager {
return; return;
} }
if let Err(request) = self if let Err(request) =
.queue self.queue
.enqueue(global, client.id(), Request { buffer, notifier }, now) .enqueue(global, client.id(), Request { buffer, notifier }, now)
{ {
request.notifier.failed(); request.notifier.failed();
} }
} }
pub(crate) fn process<R: ImportDma>(&mut self, global: DmabufGlobal, renderer: &mut R, node: Option<DrmNode>) { pub(crate) fn process<R: ImportDma>(
self.process_with(global, node, |buffer| renderer.import_dmabuf(buffer, None).is_ok()); &mut self,
global: DmabufGlobal,
renderer: &mut R,
node: Option<DrmNode>,
) {
self.process_with(global, node, |buffer| {
renderer.import_dmabuf(buffer, None).is_ok()
});
} }
pub(crate) fn process_with( pub(crate) fn process_with(
@@ -171,16 +191,17 @@ impl Ferese {
let mut backend = backend.borrow_mut(); let mut backend = backend.borrow_mut();
// Nested Ferese publishes exactly one DMA-BUF global. // Nested Ferese publishes exactly one DMA-BUF global.
if let Some(global) = self.dmabuf_imports.globals.iter().next().copied() { if let Some(global) = self.dmabuf_imports.globals.iter().next().copied() {
self.dmabuf_imports.process(global, backend.renderer(), None); self.dmabuf_imports
.process(global, backend.renderer(), None);
} }
} }
} }
if let Some(deadline) = self.dmabuf_imports.queue.deadline() { if let Some(deadline) = self.dmabuf_imports.queue.deadline() {
if self.dmabuf_imports.timer.is_none() { if self.dmabuf_imports.timer.is_none() {
match self match self.loop_handle.insert_source(
.loop_handle Timer::from_deadline(deadline),
.insert_source(Timer::from_deadline(deadline), |_, _, state| { |_, _, state| {
state.dmabuf_imports.expire(Instant::now()); state.dmabuf_imports.expire(Instant::now());
if let Some(deadline) = state.dmabuf_imports.queue.deadline() { if let Some(deadline) = state.dmabuf_imports.queue.deadline() {
TimeoutAction::ToInstant(deadline) TimeoutAction::ToInstant(deadline)
@@ -188,7 +209,8 @@ impl Ferese {
state.dmabuf_imports.timer = None; state.dmabuf_imports.timer = None;
TimeoutAction::Drop TimeoutAction::Drop
} }
}) { },
) {
Ok(token) => self.dmabuf_imports.timer = Some(token), Ok(token) => self.dmabuf_imports.timer = Some(token),
Err(error) => { Err(error) => {
tracing::warn!(%error, "could not schedule DMA-BUF import deadline"); tracing::warn!(%error, "could not schedule DMA-BUF import deadline");
@@ -234,7 +256,9 @@ mod tests {
let now = Instant::now(); let now = Instant::now();
let mut queue = Queue::default(); let mut queue = Queue::default();
for index in 0..MAX_PENDING { for index in 0..MAX_PENDING {
queue.enqueue(1, index / MAX_PER_CLIENT, index, now).unwrap(); queue
.enqueue(1, index / MAX_PER_CLIENT, index, now)
.unwrap();
} }
assert_eq!(queue.enqueue(1, 99, 100, now), Err(100)); assert_eq!(queue.enqueue(1, 99, 100, now), Err(100));
@@ -275,13 +299,18 @@ mod tests {
let fd_removed = removed.as_raw_fd(); let fd_removed = removed.as_raw_fd();
let fd_expired = expired.as_raw_fd(); let fd_expired = expired.as_raw_fd();
queue.enqueue(1, 1, removed, now).unwrap(); queue.enqueue(1, 1, removed, now).unwrap();
queue.enqueue(2, 1, expired, now + Duration::from_secs(1)).unwrap(); queue
.enqueue(2, 1, expired, now + Duration::from_secs(1))
.unwrap();
let mut failures = 0; let mut failures = 0;
queue.process(&1, |_| failures += 1); queue.process(&1, |_| failures += 1);
// SAFETY: F_GETFD observes descriptor validity without modifying it. // SAFETY: F_GETFD observes descriptor validity without modifying it.
assert_eq!(unsafe { libc::fcntl(fd_removed, libc::F_GETFD) }, -1); assert_eq!(unsafe { libc::fcntl(fd_removed, libc::F_GETFD) }, -1);
assert_ne!(unsafe { libc::fcntl(fd_expired, libc::F_GETFD) }, -1); assert_ne!(unsafe { libc::fcntl(fd_expired, libc::F_GETFD) }, -1);
queue.remove_if(|entry| entry.deadline <= now + MAX_WAIT, |_| panic!("expired early")); queue.remove_if(
|entry| entry.deadline <= now + MAX_WAIT,
|_| panic!("expired early"),
);
queue.remove_if( queue.remove_if(
|entry| entry.deadline <= now + MAX_WAIT + Duration::from_secs(1), |entry| entry.deadline <= now + MAX_WAIT + Duration::from_secs(1),
|_| failures += 1, |_| failures += 1,
+49 -18
View File
@@ -6,7 +6,9 @@ use ferese_protocols::effects::v1::server::ferese_surface_effects_v1::FereseSurf
use ferese_protocols::effects::v1::server::{ferese_effects_manager_v1, ferese_surface_effects_v1}; 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_material_manager_v1::FereseMaterialManagerV1;
use ferese_protocols::material::v1::server::ferese_surface_material_v1::FereseSurfaceMaterialV1; 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 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::backend::ClientId;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::reexports::wayland_server::{ use smithay::reexports::wayland_server::{
@@ -37,10 +39,16 @@ pub(crate) struct ResolvedMaterial {
pub shadow: [f64; 3], 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 { let shadow = match role {
SemanticRole::Panel => [1.0, 5.0, 0.04], 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::Hud => [1.0, 4.0, 0.04],
SemanticRole::Notification | SemanticRole::Modal => [3.0, 10.0, 0.09], SemanticRole::Notification | SemanticRole::Modal => [3.0, 10.0, 0.09],
}; };
@@ -48,7 +56,11 @@ pub(crate) fn resolve_material(role: SemanticRole, style: MaterialStyle, opacity
MaterialStyle::Solid => 1.0, MaterialStyle::Solid => 1.0,
MaterialStyle::Translucent => opacity.clamp(0.0, 1.0), MaterialStyle::Translucent => opacity.clamp(0.0, 1.0),
}; };
ResolvedMaterial { style, opacity, shadow } ResolvedMaterial {
style,
opacity,
shadow,
}
} }
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
@@ -167,7 +179,9 @@ impl Dispatch<FereseMaterialManagerV1, ()> for Ferese {
match request { match request {
ferese_material_manager_v1::Request::GetSurfaceMaterial { id, surface } => { ferese_material_manager_v1::Request::GetSurfaceMaterial { id, surface } => {
let attached = with_states(&surface, |states| { 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(); let effects = states.data_map.get::<SurfaceEffectsState>().unwrap();
effects.attached.swap(true, Ordering::AcqRel) effects.attached.swap(true, Ordering::AcqRel)
}); });
@@ -288,7 +302,9 @@ pub(crate) fn surface_region_opacities(surface: &WlSurface) -> Vec<u32> {
} }
fn decode_region_opacities(bytes: &[u8]) -> Option<Vec<u32>> { fn decode_region_opacities(bytes: &[u8]) -> Option<Vec<u32>> {
if !bytes.len().is_multiple_of(4) || bytes.len() > ferese_protocols::effects::v1::MAX_REGIONS * 4 { if !bytes.len().is_multiple_of(4)
|| bytes.len() > ferese_protocols::effects::v1::MAX_REGIONS * 4
{
return None; return None;
} }
bytes bytes
@@ -303,7 +319,9 @@ fn decode_region_opacities(bytes: &[u8]) -> Option<Vec<u32>> {
} }
fn decode_regions(bytes: &[u8]) -> Option<Vec<[f64; 5]>> { fn decode_regions(bytes: &[u8]) -> Option<Vec<[f64; 5]>> {
if !bytes.len().is_multiple_of(20) || bytes.len() > ferese_protocols::effects::v1::MAX_REGIONS * 20 { if !bytes.len().is_multiple_of(20)
|| bytes.len() > ferese_protocols::effects::v1::MAX_REGIONS * 20
{
return None; return None;
} }
bytes bytes
@@ -311,7 +329,8 @@ fn decode_regions(bytes: &[u8]) -> Option<Vec<[f64; 5]>> {
.0 .0
.iter() .iter()
.map(|tuple| { .map(|tuple| {
let values: [f64; 5] = std::array::from_fn(|i| f64::from(f32::from_ne_bytes(tuple.as_chunks::<4>().0[i]))); let values: [f64; 5] =
std::array::from_fn(|i| f64::from(f32::from_ne_bytes(tuple.as_chunks::<4>().0[i])));
(values[2] > 0.0 (values[2] > 0.0
&& values[3] > 0.0 && values[3] > 0.0
&& values[4] >= 0.0 && values[4] >= 0.0
@@ -363,7 +382,9 @@ impl Dispatch<FereseEffectsManagerV1, ()> for Ferese {
match request { match request {
ferese_effects_manager_v1::Request::GetSurfaceEffects { id, surface } => { ferese_effects_manager_v1::Request::GetSurfaceEffects { id, surface } => {
let already_attached = with_states(&surface, |states| { 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 states
.data_map .data_map
.get::<SurfaceEffectsState>() .get::<SurfaceEffectsState>()
@@ -416,8 +437,10 @@ impl Dispatch<FereseSurfaceEffectsV1, SurfaceEffectsUserData> for Ferese {
fusion, fusion,
opacity, opacity,
} => { } => {
let (Some(regions), Some(panel)) = (decode_regions(&regions), decode_panel_shapes(&shapes, fusion)) let (Some(regions), Some(panel)) = (
else { decode_regions(&regions),
decode_panel_shapes(&shapes, fusion),
) else {
return; return;
}; };
if panel.shapes.is_empty() != regions.is_empty() { if panel.shapes.is_empty() != regions.is_empty() {
@@ -600,17 +623,22 @@ pub(crate) fn surface_opacity(surface: &WlSurface) -> f32 {
states states
.data_map .data_map
.get::<SurfaceEffectsState>() .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 { pub(crate) fn begin_surface_dismiss(surface: &WlSurface) -> bool {
with_states(surface, |states| { with_states(surface, |states| {
states.data_map.get::<SurfaceEffectsState>().is_some_and(|effects| { states
effects.presentation_supported.load(Ordering::Acquire) .data_map
&& effects.attached.load(Ordering::Acquire) .get::<SurfaceEffectsState>()
&& !effects.dismissing.swap(true, Ordering::AcqRel) .is_some_and(|effects| {
}) effects.presentation_supported.load(Ordering::Acquire)
&& effects.attached.load(Ordering::Acquire)
&& !effects.dismissing.swap(true, Ordering::AcqRel)
})
}) })
} }
@@ -733,7 +761,10 @@ mod tests {
#[test] #[test]
fn region_opacities_validate_independent_fades() { fn region_opacities_validate_independent_fades() {
let bytes: Vec<_> = [0u32, 500, 1000].into_iter().flat_map(u32::to_ne_bytes).collect(); let bytes: Vec<_> = [0u32, 500, 1000]
.into_iter()
.flat_map(u32::to_ne_bytes)
.collect();
assert_eq!(decode_region_opacities(&bytes), Some(vec![0, 500, 1000])); assert_eq!(decode_region_opacities(&bytes), Some(vec![0, 500, 1000]));
assert_eq!(decode_region_opacities(&[]), Some(vec![])); assert_eq!(decode_region_opacities(&[]), Some(vec![]));
assert!(decode_region_opacities(&bytes[..11]).is_none()); assert!(decode_region_opacities(&bytes[..11]).is_none());
+64 -20
View File
@@ -19,8 +19,12 @@ pub fn intersection(a: Rect, b: Rect) -> Option<Rect> {
pub fn clamp_to_work(mut rect: Rect, work: Rect) -> Rect { pub fn clamp_to_work(mut rect: Rect, work: Rect) -> Rect {
// Preserve client minimum sizes even when the work area is smaller. // Preserve client minimum sizes even when the work area is smaller.
rect.x = rect.x.clamp(work.x, work.x + (work.width - rect.width).max(0.0)); rect.x = rect
rect.y = rect.y.clamp(work.y, work.y + (work.height - rect.height).max(0.0)); .x
.clamp(work.x, work.x + (work.width - rect.width).max(0.0));
rect.y = rect
.y
.clamp(work.y, work.y + (work.height - rect.height).max(0.0));
rect rect
} }
@@ -35,14 +39,27 @@ pub fn centered(size: (f64, f64), anchor: Rect) -> Rect {
pub fn anchored_size(rect: Rect, size: (f64, f64), left: bool, top: bool) -> Rect { pub fn anchored_size(rect: Rect, size: (f64, f64), left: bool, top: bool) -> Rect {
Rect::new( Rect::new(
if left { rect.x + rect.width - size.0 } else { rect.x }, if left {
if top { rect.y + rect.height - size.1 } else { rect.y }, rect.x + rect.width - size.0
} else {
rect.x
},
if top {
rect.y + rect.height - size.1
} else {
rect.y
},
size.0, size.0,
size.1, size.1,
) )
} }
pub fn min_overlap(size: (f64, f64), work: Rect, others: &[Rect], anchor: (f64, f64)) -> Option<Rect> { pub fn min_overlap(
size: (f64, f64),
work: Rect,
others: &[Rect],
anchor: (f64, f64),
) -> Option<Rect> {
let (width, height) = size; let (width, height) = size;
if !width.is_finite() if !width.is_finite()
|| !height.is_finite() || !height.is_finite()
@@ -77,12 +94,15 @@ pub fn min_overlap(size: (f64, f64), work: Rect, others: &[Rect], anchor: (f64,
.filter_map(|other| intersection(rect, *other)) .filter_map(|other| intersection(rect, *other))
.map(|r| r.width * r.height) .map(|r| r.width * r.height)
.sum::<f64>(); .sum::<f64>();
let distance = (x + width / 2.0 - anchor.0).powi(2) + (y + height / 2.0 - anchor.1).powi(2); let distance =
(x + width / 2.0 - anchor.0).powi(2) + (y + height / 2.0 - anchor.1).powi(2);
let cost = (overlap, distance); let cost = (overlap, distance);
if best if best.as_ref().is_none_or(|(old, _)| {
.as_ref() cost.0
.is_none_or(|(old, _)| cost.0.total_cmp(&old.0).then(cost.1.total_cmp(&old.1)).is_lt()) .total_cmp(&old.0)
{ .then(cost.1.total_cmp(&old.1))
.is_lt()
}) {
best = Some((cost, rect)); best = Some((cost, rect));
} }
} }
@@ -97,7 +117,10 @@ pub struct Cascade {
impl Cascade { impl Cascade {
pub fn place(&mut self, output: &str, work: Rect, size: (f64, f64)) -> Rect { pub fn place(&mut self, output: &str, work: Rect, size: (f64, f64)) -> Rect {
let position = self.positions.entry(output.to_owned()).or_insert((work.x, work.y)); let position = self
.positions
.entry(output.to_owned())
.or_insert((work.x, work.y));
if position.0 < work.x if position.0 < work.x
|| position.1 < work.y || position.1 < work.y
|| position.0 + size.0 > work.x + work.width || position.0 + size.0 > work.x + work.width
@@ -132,7 +155,10 @@ impl Remembered {
pub fn restore(&self, outputs: &[(String, Rect)]) -> Option<Rect> { pub fn restore(&self, outputs: &[(String, Rect)]) -> Option<Rect> {
let work = outputs.iter().find(|(name, _)| *name == self.output)?.1; let work = outputs.iter().find(|(name, _)| *name == self.output)?.1;
if !self.fractions.iter().all(|v| v.is_finite()) || self.fractions[2] <= 0.0 || self.fractions[3] <= 0.0 { if !self.fractions.iter().all(|v| v.is_finite())
|| self.fractions[2] <= 0.0
|| self.fractions[3] <= 0.0
{
return None; return None;
} }
let [x, y, w, h] = self.fractions; let [x, y, w, h] = self.fractions;
@@ -142,11 +168,15 @@ impl Remembered {
w * work.width, w * work.width,
h * work.height, h * work.height,
); );
if ![rect.x, rect.y, rect.width, rect.height].iter().all(|v| v.is_finite()) { if ![rect.x, rect.y, rect.width, rect.height]
.iter()
.all(|v| v.is_finite())
{
return None; return None;
} }
let visible = outputs.iter().any(|(_, area)| { let visible = outputs.iter().any(|(_, area)| {
intersection(rect, *area).is_some_and(|r| r.width * r.height >= rect.width * rect.height * 0.25) intersection(rect, *area)
.is_some_and(|r| r.width * r.height >= rect.width * rect.height * 0.25)
}); });
visible.then_some(rect) visible.then_some(rect)
} }
@@ -169,8 +199,13 @@ struct SaveThread {
} }
impl SaveThread { impl SaveThread {
fn start(mut persist: impl FnMut(&Path, &Memory) -> std::io::Result<()> + Send + 'static) -> std::io::Result<Self> { fn start(
let shared = std::sync::Arc::new((std::sync::Mutex::new(SaveState::default()), std::sync::Condvar::new())); mut persist: impl FnMut(&Path, &Memory) -> std::io::Result<()> + Send + 'static,
) -> std::io::Result<Self> {
let shared = std::sync::Arc::new((
std::sync::Mutex::new(SaveState::default()),
std::sync::Condvar::new(),
));
let worker = shared.clone(); let worker = shared.clone();
let thread = std::thread::Builder::new() let thread = std::thread::Builder::new()
.name("floating-geometry-save".into()) .name("floating-geometry-save".into())
@@ -355,7 +390,9 @@ pub fn snap_lines(raw: Rect, work: Rect, others: &[(u64, Rect)]) -> (Vec<SnapLin
}, },
]; ];
for &(target, rect) in others { for &(target, rect) in others {
if raw.y - SNAP_DISTANCE <= rect.y + rect.height && raw.y + raw.height + SNAP_DISTANCE >= rect.y { if raw.y - SNAP_DISTANCE <= rect.y + rect.height
&& raw.y + raw.height + SNAP_DISTANCE >= rect.y
{
xs.extend([ xs.extend([
SnapLine { SnapLine {
target, target,
@@ -367,7 +404,9 @@ pub fn snap_lines(raw: Rect, work: Rect, others: &[(u64, Rect)]) -> (Vec<SnapLin
}, },
]); ]);
} }
if raw.x - SNAP_DISTANCE <= rect.x + rect.width && raw.x + raw.width + SNAP_DISTANCE >= rect.x { if raw.x - SNAP_DISTANCE <= rect.x + rect.width
&& raw.x + raw.width + SNAP_DISTANCE >= rect.x
{
ys.extend([ ys.extend([
SnapLine { SnapLine {
target, target,
@@ -420,7 +459,9 @@ mod tests {
memory memory
}; };
worker.queue(PathBuf::from("unused"), snapshot("first")); worker.queue(PathBuf::from("unused"), snapshot("first"));
started_rx.recv_timeout(std::time::Duration::from_secs(2)).unwrap(); started_rx
.recv_timeout(std::time::Duration::from_secs(2))
.unwrap();
worker.queue(PathBuf::from("unused"), snapshot("superseded")); worker.queue(PathBuf::from("unused"), snapshot("superseded"));
worker.queue(PathBuf::from("unused"), snapshot("last")); worker.queue(PathBuf::from("unused"), snapshot("last"));
assert_eq!(*written.lock().unwrap(), vec!["first"]); assert_eq!(*written.lock().unwrap(), vec!["first"]);
@@ -511,7 +552,10 @@ mod tests {
}; };
let placed = min_overlap(size, Rect::new(0., 0., 16., 16.), &others, (8., 8.)).unwrap(); let placed = min_overlap(size, Rect::new(0., 0., 16., 16.), &others, (8., 8.)).unwrap();
let brute = (0..=16 - size.0 as i32) let brute = (0..=16 - size.0 as i32)
.flat_map(|x| (0..=16 - size.1 as i32).map(move |y| Rect::new(x as f64, y as f64, size.0, size.1))) .flat_map(|x| {
(0..=16 - size.1 as i32)
.map(move |y| Rect::new(x as f64, y as f64, size.0, size.1))
})
.map(overlap) .map(overlap)
.min_by(f64::total_cmp) .min_by(f64::total_cmp)
.unwrap(); .unwrap();
+18 -4
View File
@@ -3,7 +3,11 @@ use std::time::Duration;
use crate::config::FocusEffectSettings; use crate::config::FocusEffectSettings;
use ferese_layout::WindowId; use ferese_layout::WindowId;
pub(crate) fn target(settings: FocusEffectSettings, active: Option<WindowId>, window: WindowId) -> f64 { pub(crate) fn target(
settings: FocusEffectSettings,
active: Option<WindowId>,
window: WindowId,
) -> f64 {
if !settings.has_effects() || active == Some(window) { if !settings.has_effects() || active == Some(window) {
1.0 1.0
} else { } else {
@@ -16,12 +20,18 @@ impl FocusEffectSettings {
self.enabled && (self.active_opacity != self.inactive_opacity || self.inactive_dim != 0.0) self.enabled && (self.active_opacity != self.inactive_opacity || self.inactive_dim != 0.0)
} }
pub(crate) fn resolve(self, progress: f64, has_active: bool, overview_opacity: f32) -> (f32, f64) { pub(crate) fn resolve(
self,
progress: f64,
has_active: bool,
overview_opacity: f32,
) -> (f32, f64) {
if !self.enabled { if !self.enabled {
return (1.0, 0.0); return (1.0, 0.0);
} }
let progress = progress.clamp(0.0, 1.0); let progress = progress.clamp(0.0, 1.0);
let alpha = self.inactive_opacity + (self.active_opacity - self.inactive_opacity) * progress; let alpha =
self.inactive_opacity + (self.active_opacity - self.inactive_opacity) * progress;
let dim = if has_active { let dim = if has_active {
self.inactive_dim * (1.0 - progress) * (1.0 - f64::from(overview_opacity)) self.inactive_dim * (1.0 - progress) * (1.0 - f64::from(overview_opacity))
} else { } else {
@@ -87,7 +97,11 @@ impl BoundedFade {
pub fn advance_visual(&mut self, target: f64, delta: Duration, duration_ms: f64) -> bool { pub fn advance_visual(&mut self, target: f64, delta: Duration, duration_ms: f64) -> bool {
self.advance( self.advance(
target, target,
if self.target != target { Duration::ZERO } else { delta }, if self.target != target {
Duration::ZERO
} else {
delta
},
duration_ms, duration_ms,
) )
} }
+31 -10
View File
@@ -68,7 +68,8 @@ impl FrameScheduler {
// A cold GPU is less predictable. Start immediately on idle wakeup. // A cold GPU is less predictable. Start immediately on idle wakeup.
self.interval self.interval
} else { } else {
(self.render_estimate + self.render_variance * 2 + TIMER_MARGIN + self.extra_margin).min(self.interval) (self.render_estimate + self.render_variance * 2 + TIMER_MARGIN + self.extra_margin)
.min(self.interval)
} }
} }
@@ -95,7 +96,10 @@ impl FrameScheduler {
} }
let requested_at = self.requested_at?; let requested_at = self.requested_at?;
if self.last_presented.is_some_and(|last| last > now + self.interval) { if self
.last_presented
.is_some_and(|last| last > now + self.interval)
{
self.last_presented = None; self.last_presented = None;
self.last_target = None; self.last_target = None;
} }
@@ -148,8 +152,12 @@ impl FrameScheduler {
let ratio = (elapsed.as_secs_f64() / 0.5).clamp(0.01, 1.0); let ratio = (elapsed.as_secs_f64() / 0.5).clamp(0.01, 1.0);
let deviation = sample.saturating_sub(self.render_estimate); let deviation = sample.saturating_sub(self.render_estimate);
let variance_ratio = (elapsed.as_secs_f64() / 6.0).clamp(0.001, 0.1); let variance_ratio = (elapsed.as_secs_f64() / 6.0).clamp(0.001, 0.1);
self.render_variance = self.render_variance.mul_f64(1.0 - variance_ratio).max(deviation); self.render_variance = self
self.render_estimate = self.render_estimate.mul_f64(1.0 - ratio) + sample.mul_f64(ratio); .render_variance
.mul_f64(1.0 - variance_ratio)
.max(deviation);
self.render_estimate =
self.render_estimate.mul_f64(1.0 - ratio) + sample.mul_f64(ratio);
} }
} }
@@ -168,7 +176,8 @@ impl FrameScheduler {
self.last_target = Some(timestamp); self.last_target = Some(timestamp);
let late = timestamp.saturating_sub(plan.present_at); let late = timestamp.saturating_sub(plan.present_at);
if plan.phase_known && late > self.interval / 2 { if plan.phase_known && late > self.interval / 2 {
let missed = (late.as_nanos() + self.interval.as_nanos() / 2) / self.interval.as_nanos(); let missed =
(late.as_nanos() + self.interval.as_nanos() / 2) / self.interval.as_nanos();
self.missed_deadlines = self.missed_deadlines.saturating_add(missed as u64); self.missed_deadlines = self.missed_deadlines.saturating_add(missed as u64);
// CPU submission timings may omit asynchronous GPU work. Increase // CPU submission timings may omit asynchronous GPU work. Increase
// the margin promptly after a real miss; relax it slowly afterward. // the margin promptly after a real miss; relax it slowly afterward.
@@ -226,7 +235,8 @@ impl FrameScheduler {
// the next refresh look too early and halve a client's frame rate. // the next refresh look too early and halve a client's frame rate.
self.last_callback = Some(self.last_presented.map_or(now, |last| { self.last_callback = Some(self.last_presented.map_or(now, |last| {
let tolerance = Duration::from_micros(500).min(self.interval / 8); let tolerance = Duration::from_micros(500).min(self.interval / 8);
let slots = (now.saturating_sub(last) + tolerance).as_nanos() / self.interval.as_nanos(); let slots =
(now.saturating_sub(last) + tolerance).as_nanos() / self.interval.as_nanos();
last + self.interval.mul_f64(slots as f64) last + self.interval.mul_f64(slots as f64)
})); }));
} }
@@ -308,7 +318,10 @@ mod tests {
scheduler.callback_sent(ms(110)); scheduler.callback_sent(ms(110));
scheduler.request(ms(108)); scheduler.request(ms(108));
assert_eq!(scheduler.plan(ms(108)).unwrap().present_at, ms(120)); assert_eq!(scheduler.plan(ms(108)).unwrap().present_at, ms(120));
assert_eq!(scheduler.callback_deadline(ms(111), ms(110)).unwrap(), ms(120)); assert_eq!(
scheduler.callback_deadline(ms(111), ms(110)).unwrap(),
ms(120)
);
} }
#[test] #[test]
@@ -319,7 +332,9 @@ mod tests {
let unchanged = scheduler.plan(ms(101)).unwrap(); let unchanged = scheduler.plan(ms(101)).unwrap();
scheduler.begin(); scheduler.begin();
scheduler.rendered(unchanged, ms(108), ms(1), false); scheduler.rendered(unchanged, ms(108), ms(1), false);
let callback_at = scheduler.callback_deadline(ms(108), unchanged.present_at).unwrap(); let callback_at = scheduler
.callback_deadline(ms(108), unchanged.present_at)
.unwrap();
assert_eq!(callback_at, ms(110)); assert_eq!(callback_at, ms(110));
scheduler.request(ms(109)); scheduler.request(ms(109));
@@ -330,13 +345,19 @@ mod tests {
// The old timer is dispatched late, after a new frame was queued. // The old timer is dispatched late, after a new frame was queued.
// Neither that timer nor the predicted refresh can release callbacks. // Neither that timer nor the predicted refresh can release callbacks.
assert_eq!(scheduler.callback_deadline(ms(118), callback_at), None); assert_eq!(scheduler.callback_deadline(ms(118), callback_at), None);
assert_eq!(scheduler.callback_deadline(ms(120), changed.present_at), None); assert_eq!(
scheduler.callback_deadline(ms(120), changed.present_at),
None
);
assert_eq!(scheduler.callback_deadline(ms(129), callback_at), None); assert_eq!(scheduler.callback_deadline(ms(129), callback_at), None);
scheduler.presented(ms(130)); scheduler.presented(ms(130));
assert_eq!(scheduler.callback_deadline(ms(130), ms(130)), Some(ms(130))); assert_eq!(scheduler.callback_deadline(ms(130), ms(130)), Some(ms(130)));
scheduler.callback_sent(ms(130)); scheduler.callback_sent(ms(130));
assert_eq!(scheduler.callback_deadline(ms(130), callback_at), Some(ms(140))); assert_eq!(
scheduler.callback_deadline(ms(130), callback_at),
Some(ms(140))
);
} }
#[test] #[test]
+21 -5
View File
@@ -30,7 +30,11 @@ impl Ramp {
pub(crate) fn at_temperature(&self, temperature: u16) -> Self { pub(crate) fn at_temperature(&self, temperature: u16) -> Self {
let mut ramp = self.clone(); let mut ramp = self.clone();
for (channel, gain) in ramp.0.chunks_exact_mut(self.size()).zip(temperature_gains(temperature)) { for (channel, gain) in ramp
.0
.chunks_exact_mut(self.size())
.zip(temperature_gains(temperature))
{
for value in channel { for value in channel {
*value = (f64::from(*value) * gain).round() as u16; *value = (f64::from(*value) * gain).round() as u16;
} }
@@ -45,14 +49,18 @@ impl Ramp {
self.0 self.0
.iter() .iter()
.zip(&target.0) .zip(&target.0)
.map(|(&from, &to)| (f64::from(from) + (f64::from(to) - f64::from(from)) * progress).round() as u16) .map(|(&from, &to)| {
(f64::from(from) + (f64::from(to) - f64::from(from)) * progress).round() as u16
})
.collect(), .collect(),
) )
} }
pub(crate) fn read(file: File, size: usize) -> io::Result<Self> { pub(crate) fn read(file: File, size: usize) -> io::Result<Self> {
let metadata = file.metadata()?; let metadata = file.metadata()?;
let length = size.checked_mul(6).filter(|_| size > 0 && size <= MAX_GAMMA_SIZE); let length = size
.checked_mul(6)
.filter(|_| size > 0 && size <= MAX_GAMMA_SIZE);
let Some(length) = length else { let Some(length) = length else {
return Err(io::ErrorKind::InvalidData.into()); return Err(io::ErrorKind::InvalidData.into());
}; };
@@ -73,7 +81,11 @@ impl Ramp {
pub(crate) fn channels(&self) -> (&[u16], &[u16], &[u16]) { pub(crate) fn channels(&self) -> (&[u16], &[u16], &[u16]) {
let size = self.0.len() / 3; let size = self.0.len() / 3;
(&self.0[..size], &self.0[size..2 * size], &self.0[2 * size..]) (
&self.0[..size],
&self.0[size..2 * size],
&self.0[2 * size..],
)
} }
} }
@@ -89,5 +101,9 @@ fn temperature_gains(kelvin: u16) -> [f64; 3] {
} }
}; };
let temperature = f64::from(kelvin.clamp(1000, 6500)) / 100.; let temperature = f64::from(kelvin.clamp(1000, 6500)) / 100.;
[1., green(temperature) / green(65.), blue(temperature) / blue(65.)] [
1.,
green(temperature) / green(65.),
blue(temperature) / blue(65.),
]
} }
+12 -4
View File
@@ -49,7 +49,11 @@ impl Swipe {
} }
} }
pub fn finish_at(&mut self, cancelled: bool, time: std::time::Duration) -> Option<SwipeDirection> { pub fn finish_at(
&mut self,
cancelled: bool,
time: std::time::Duration,
) -> Option<SwipeDirection> {
let Some(direction) = self.direction else { let Some(direction) = self.direction else {
return self.finish(cancelled); return self.finish(cancelled);
}; };
@@ -76,11 +80,14 @@ impl Swipe {
}; };
// The preview is bounded. Its visible velocity is zero when pushing // The preview is bounded. Its visible velocity is zero when pushing
// farther beyond either endpoint, even if the fingers keep moving. // farther beyond either endpoint, even if the fingers keep moving.
if (progress == 0.0 && self.release_velocity < 0.0) || (progress == 1.0 && self.release_velocity > 0.0) { if (progress == 0.0 && self.release_velocity < 0.0)
|| (progress == 1.0 && self.release_velocity > 0.0)
{
self.release_velocity = 0.0; self.release_velocity = 0.0;
} }
self.delta = None; self.delta = None;
(!cancelled && ferese_animation::gesture::project(progress, self.release_velocity, 0.997) >= 0.5) (!cancelled
&& ferese_animation::gesture::project(progress, self.release_velocity, 0.997) >= 0.5)
.then_some(direction) .then_some(direction)
} }
@@ -139,7 +146,8 @@ impl Swipe {
if cancelled { if cancelled {
return None; return None;
} }
dominant_direction(x, y, self.threshold).filter(|direction| Some(*direction) == self.direction) dominant_direction(x, y, self.threshold)
.filter(|direction| Some(*direction) == self.direction)
} }
} }
+16 -5
View File
@@ -1,8 +1,9 @@
use smithay::desktop::Window; use smithay::desktop::Window;
use smithay::input::pointer::{ use smithay::input::pointer::{
AxisFrame, ButtonEvent, GestureHoldBeginEvent, GestureHoldEndEvent, GesturePinchBeginEvent, GesturePinchEndEvent, AxisFrame, ButtonEvent, GestureHoldBeginEvent, GestureHoldEndEvent, GesturePinchBeginEvent,
GesturePinchUpdateEvent, GestureSwipeBeginEvent, GestureSwipeEndEvent, GestureSwipeUpdateEvent, GrabStartData, GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent, GestureSwipeEndEvent,
MotionEvent, PointerGrab, PointerInnerHandle, RelativeMotionEvent, GestureSwipeUpdateEvent, GrabStartData, MotionEvent, PointerGrab, PointerInnerHandle,
RelativeMotionEvent,
}; };
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::utils::{Logical, Point, Size}; use smithay::utils::{Logical, Point, Size};
@@ -61,7 +62,12 @@ impl PointerGrab<Ferese> for MoveSurfaceGrab {
handle.relative_motion(data, focus, event); 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); handle.button(data, event);
if handle.current_pressed().is_empty() { if handle.current_pressed().is_empty() {
self.finished = true; self.finished = true;
@@ -70,7 +76,12 @@ 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); handle.axis(data, frame);
} }
+66 -19
View File
@@ -1,8 +1,9 @@
use smithay::desktop::Window; use smithay::desktop::Window;
use smithay::input::pointer::{ use smithay::input::pointer::{
AxisFrame, ButtonEvent, GestureHoldBeginEvent, GestureHoldEndEvent, GesturePinchBeginEvent, GesturePinchEndEvent, AxisFrame, ButtonEvent, GestureHoldBeginEvent, GestureHoldEndEvent, GesturePinchBeginEvent,
GesturePinchUpdateEvent, GestureSwipeBeginEvent, GestureSwipeEndEvent, GestureSwipeUpdateEvent, GrabStartData, GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent, GestureSwipeEndEvent,
MotionEvent, PointerGrab, PointerInnerHandle, RelativeMotionEvent, GestureSwipeUpdateEvent, GrabStartData, MotionEvent, PointerGrab, PointerInnerHandle,
RelativeMotionEvent,
}; };
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel; use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
@@ -48,7 +49,9 @@ impl ResizeEdge {
fn left(self) -> bool { fn left(self) -> bool {
matches!( matches!(
self.0, 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
) )
} }
@@ -64,7 +67,9 @@ impl ResizeEdge {
fn top(self) -> bool { fn top(self) -> bool {
matches!( matches!(
self.0, 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
) )
} }
@@ -122,8 +127,12 @@ impl PointerGrab<Ferese> for ResizeSurfaceGrab {
let initial = self.initial_rect; let initial = self.initial_rect;
let raw_left = initial.loc.x as f64 + if self.edges.left() { delta.x } else { 0. }; let raw_left = initial.loc.x as f64 + if self.edges.left() { delta.x } else { 0. };
let raw_top = initial.loc.y as f64 + if self.edges.top() { delta.y } else { 0. }; let raw_top = initial.loc.y as f64 + if self.edges.top() { delta.y } else { 0. };
let raw_right = initial.loc.x as f64 + initial.size.w as f64 + if self.edges.right() { delta.x } else { 0. }; let raw_right = initial.loc.x as f64
let raw_bottom = initial.loc.y as f64 + initial.size.h as f64 + if self.edges.bottom() { delta.y } else { 0. }; + initial.size.w as f64
+ if self.edges.right() { delta.x } else { 0. };
let raw_bottom = initial.loc.y as f64
+ initial.size.h as f64
+ if self.edges.bottom() { delta.y } else { 0. };
let raw = ferese_layout::Rect::new( let raw = ferese_layout::Rect::new(
raw_left, raw_left,
raw_top, raw_top,
@@ -136,22 +145,36 @@ impl PointerGrab<Ferese> for ResizeSurfaceGrab {
self.snap_y.clear(); self.snap_y.clear();
} else { } else {
if self.edges.left() || self.edges.right() { if self.edges.left() || self.edges.right() {
let moving = if self.edges.left() { raw_left } else { raw_right }; let moving = if self.edges.left() {
raw_left
} else {
raw_right
};
delta.x += self.snap_x.apply(moving, 0., &xs) - moving; delta.x += self.snap_x.apply(moving, 0., &xs) - moving;
} }
if self.edges.top() || self.edges.bottom() { if self.edges.top() || self.edges.bottom() {
let moving = if self.edges.top() { raw_top } else { raw_bottom }; let moving = if self.edges.top() {
raw_top
} else {
raw_bottom
};
delta.y += self.snap_y.apply(moving, 0., &ys) - moving; delta.y += self.snap_y.apply(moving, 0., &ys) - moving;
} }
} }
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 (minimum, maximum) = with_states(surface.wl_surface(), |states| {
let mut cached = states.cached_state.get::<SurfaceCachedState>(); let mut cached = states.cached_state.get::<SurfaceCachedState>();
let state = cached.current(); let state = cached.current();
(state.min_size, state.max_size) (state.min_size, state.max_size)
}); });
let (minimum, maximum) = let (minimum, maximum) = data.effective_size_constraints(
data.effective_size_constraints(&self.window, (minimum.w, minimum.h), (maximum.w, maximum.h)); &self.window,
(minimum.w, minimum.h),
(maximum.w, maximum.h),
);
self.last_size = constrained_size( self.last_size = constrained_size(
self.initial_rect.size, self.initial_rect.size,
delta, delta,
@@ -193,13 +216,21 @@ impl PointerGrab<Ferese> for ResizeSurfaceGrab {
handle.relative_motion(data, focus, event); 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); handle.button(data, event);
if handle.current_pressed().is_empty() { if handle.current_pressed().is_empty() {
self.finished = true; self.finished = true;
data.remember_floating(&self.window); data.remember_floating(&self.window);
handle.unset_grab(self, data, event.serial, event.time, 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| { surface.with_pending_state(|state| {
state.states.unset(xdg_toplevel::State::Resizing); state.states.unset(xdg_toplevel::State::Resizing);
state.size = Some(self.last_size); state.size = Some(self.last_size);
@@ -208,7 +239,12 @@ 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); handle.axis(data, frame);
} }
@@ -297,13 +333,20 @@ impl PointerGrab<Ferese> for ResizeSurfaceGrab {
return; 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| { surface.with_pending_state(|state| {
state.states.unset(xdg_toplevel::State::Resizing); state.states.unset(xdg_toplevel::State::Resizing);
state.size = Some(self.initial_rect.size); state.size = Some(self.initial_rect.size);
}); });
surface.send_pending_configure(); surface.send_pending_configure();
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,
);
} }
} }
@@ -357,11 +400,15 @@ fn resized_rect(
let mut location = initial.loc; let mut location = initial.loc;
if edges.left() { if edges.left() {
location.x = location.x.saturating_add(initial.size.w.saturating_sub(size.w)); location.x = location
.x
.saturating_add(initial.size.w.saturating_sub(size.w));
} }
if edges.top() { if edges.top() {
location.y = location.y.saturating_add(initial.size.h.saturating_sub(size.h)); location.y = location
.y
.saturating_add(initial.size.h.saturating_sub(size.h));
} }
Rectangle::new(location, size) Rectangle::new(location, size)
+57 -15
View File
@@ -40,7 +40,9 @@ impl<C: PartialEq> InputHistory<C> {
fn validate(&self, serial: Serial, client: &C, now: Instant) -> Option<Instant> { fn validate(&self, serial: Serial, client: &C, now: Instant) -> Option<Instant> {
self.events.iter().rev().find_map(|(event, owner, time)| { self.events.iter().rev().find_map(|(event, owner, time)| {
(*event == serial && owner == client && now.saturating_duration_since(*time) <= TOKEN_MAX_AGE) (*event == serial
&& owner == client
&& now.saturating_duration_since(*time) <= TOKEN_MAX_AGE)
.then_some(*time) .then_some(*time)
}) })
} }
@@ -55,7 +57,8 @@ impl Ferese {
} }
if let Some(client) = surface.and_then(|surface| surface.client()) { if let Some(client) = surface.and_then(|surface| surface.client()) {
self.activation_inputs.record(serial, client.id(), Instant::now()); self.activation_inputs
.record(serial, client.id(), Instant::now());
} }
} }
} }
@@ -86,14 +89,23 @@ impl XdgActivationHandler for Ferese {
return false; return false;
} }
let Some(time) = self.activation_inputs.validate(*serial, client, Instant::now()) else { let Some(time) = self
.activation_inputs
.validate(*serial, client, Instant::now())
else {
return false; return false;
}; };
data.user_data.insert_if_missing(|| ApprovedInteraction(time)); data.user_data
.insert_if_missing(|| ApprovedInteraction(time));
true true
} }
fn request_activation(&mut self, token: XdgActivationToken, data: XdgActivationTokenData, surface: WlSurface) { fn request_activation(
&mut self,
token: XdgActivationToken,
data: XdgActivationTokenData,
surface: WlSurface,
) {
self.xdg_activation_state.remove_token(&token); self.xdg_activation_state.remove_token(&token);
if self.session_lock.active() || self.input_capture.captures(1) { if self.session_lock.active() || self.input_capture.captures(1) {
return; return;
@@ -152,7 +164,9 @@ mod tests {
wire.write_all(&bytes).unwrap(); wire.write_all(&bytes).unwrap();
}; };
send(&mut wire, 1, 1, &[2]); // wl_display.get_registry send(&mut wire, 1, 1, &[2]); // wl_display.get_registry
events.dispatch(Duration::from_millis(1), &mut state).unwrap(); events
.dispatch(Duration::from_millis(1), &mut state)
.unwrap();
state.display_handle.flush_clients().unwrap(); state.display_handle.flush_clients().unwrap();
wire.set_nonblocking(true).unwrap(); wire.set_nonblocking(true).unwrap();
let mut globals = Vec::new(); let mut globals = Vec::new();
@@ -162,10 +176,16 @@ mod tests {
let mut seat_name = None; let mut seat_name = None;
while offset < globals.len() { while offset < globals.len() {
let header = u32::from_ne_bytes(globals[offset + 4..offset + 8].try_into().unwrap()); let header = u32::from_ne_bytes(globals[offset + 4..offset + 8].try_into().unwrap());
if u32::from_ne_bytes(globals[offset..offset + 4].try_into().unwrap()) == 2 && header & 0xffff == 0 { if u32::from_ne_bytes(globals[offset..offset + 4].try_into().unwrap()) == 2
let length = u32::from_ne_bytes(globals[offset + 12..offset + 16].try_into().unwrap()) as usize; && header & 0xffff == 0
{
let length =
u32::from_ne_bytes(globals[offset + 12..offset + 16].try_into().unwrap())
as usize;
if &globals[offset + 16..offset + 16 + length] == b"wl_seat\0" { if &globals[offset + 16..offset + 16 + length] == b"wl_seat\0" {
seat_name = Some(u32::from_ne_bytes(globals[offset + 8..offset + 12].try_into().unwrap())); seat_name = Some(u32::from_ne_bytes(
globals[offset + 8..offset + 12].try_into().unwrap(),
));
} }
} }
@@ -180,7 +200,9 @@ mod tests {
); );
args.extend([7, 3]); args.extend([7, 3]);
send(&mut wire, 2, 0, &args); send(&mut wire, 2, 0, &args);
events.dispatch(Duration::from_millis(1), &mut state).unwrap(); events
.dispatch(Duration::from_millis(1), &mut state)
.unwrap();
let seat: WlSeat = state let seat: WlSeat = state
.seat .seat
.client_seats(&launcher) .client_seats(&launcher)
@@ -208,7 +230,9 @@ mod tests {
!state.token_created(token.clone(), data()), !state.token_created(token.clone(), data()),
"fresh token renewed stale input" "fresh token renewed stale input"
); );
state.activation_inputs.record(serial, launcher.id(), Instant::now()); state
.activation_inputs
.record(serial, launcher.id(), Instant::now());
let approved = data(); let approved = data();
assert!(state.token_created(token.clone(), approved.clone())); assert!(state.token_created(token.clone(), approved.clone()));
assert!(approved.user_data.get::<ApprovedInteraction>().is_some()); assert!(approved.user_data.get::<ApprovedInteraction>().is_some());
@@ -227,7 +251,15 @@ mod tests {
// The proof is attached to the token, not the recipient identity. This // The proof is attached to the token, not the recipient identity. This
// is what lets a validated launcher hand it to a different application. // is what lets a validated launcher hand it to a different application.
assert_ne!(approved.client_id, Some(recipient.id())); assert_ne!(approved.client_id, Some(recipient.id()));
assert!(approved.user_data.get::<ApprovedInteraction>().unwrap().0.elapsed() < TOKEN_MAX_AGE); assert!(
approved
.user_data
.get::<ApprovedInteraction>()
.unwrap()
.0
.elapsed()
< TOKEN_MAX_AGE
);
// A valid handoff must not focus a recipient before its first buffer. // A valid handoff must not focus a recipient before its first buffer.
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_wm_base::XdgWmBase; use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_wm_base::XdgWmBase;
@@ -241,12 +273,18 @@ mod tests {
) )
.unwrap(); .unwrap();
let shell = recipient let shell = recipient
.create_resource::<XdgWmBase, XdgWmBaseUserData, Ferese>(&state.display_handle, 6, Default::default()) .create_resource::<XdgWmBase, XdgWmBaseUserData, Ferese>(
&state.display_handle,
6,
Default::default(),
)
.unwrap(); .unwrap();
send(&mut recipient_wire, compositor.id().protocol_id(), 0, &[2]); send(&mut recipient_wire, compositor.id().protocol_id(), 0, &[2]);
send(&mut recipient_wire, shell.id().protocol_id(), 2, &[3, 2]); send(&mut recipient_wire, shell.id().protocol_id(), 2, &[3, 2]);
send(&mut recipient_wire, 3, 1, &[4]); send(&mut recipient_wire, 3, 1, &[4]);
events.dispatch(Duration::from_millis(1), &mut state).unwrap(); events
.dispatch(Duration::from_millis(1), &mut state)
.unwrap();
let target = state let target = state
.space .space
.elements() .elements()
@@ -272,7 +310,11 @@ mod tests {
assert_eq!(history.validate(Serial::from(13), &"launcher", now), None); assert_eq!(history.validate(Serial::from(13), &"launcher", now), None);
assert_eq!(history.validate(serial, &"other-client", now), None); assert_eq!(history.validate(serial, &"other-client", now), None);
assert_eq!( assert_eq!(
history.validate(serial, &"launcher", now + TOKEN_MAX_AGE + Duration::from_millis(1)), history.validate(
serial,
&"launcher",
now + TOKEN_MAX_AGE + Duration::from_millis(1)
),
None None
); );
// An approved token retains the interaction time across client handoff. // An approved token retains the interaction time across client handoff.
+2 -1
View File
@@ -5,7 +5,8 @@ use smithay::reexports::wayland_server::protocol::wl_buffer;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::wayland::buffer::BufferHandler; use smithay::wayland::buffer::BufferHandler;
use smithay::wayland::compositor::{ use smithay::wayland::compositor::{
CompositorClientState, CompositorHandler, CompositorState, get_parent, get_role, is_sync_subsurface, CompositorClientState, CompositorHandler, CompositorState, get_parent, get_role,
is_sync_subsurface,
}; };
use smithay::wayland::dmabuf::{DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier}; use smithay::wayland::dmabuf::{DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier};
use smithay::wayland::shm::{ShmHandler, ShmState}; use smithay::wayland::shm::{ShmHandler, ShmState};
+2 -1
View File
@@ -1,6 +1,7 @@
use crate::Ferese; use crate::Ferese;
use smithay::reexports::wayland_server::{ use smithay::reexports::wayland_server::{
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, backend::ClientId, protocol as wl, Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, backend::ClientId,
protocol as wl,
}; };
use smithay::reexports::{ use smithay::reexports::{
wayland_protocols as protocols, wayland_protocols_misc as misc, wayland_protocols_wlr as wlr, wayland_protocols as protocols, wayland_protocols_misc as misc, wayland_protocols_wlr as wlr,
+3 -1
View File
@@ -5,7 +5,9 @@ use smithay::reexports::wayland_protocols_wlr::gamma_control::v1::server::{
zwlr_gamma_control_manager_v1::{self, ZwlrGammaControlManagerV1}, zwlr_gamma_control_manager_v1::{self, ZwlrGammaControlManagerV1},
zwlr_gamma_control_v1::{self, ZwlrGammaControlV1}, zwlr_gamma_control_v1::{self, ZwlrGammaControlV1},
}; };
use smithay::reexports::wayland_server::{Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource}; use smithay::reexports::wayland_server::{
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
};
use crate::{Ferese, backends::direct::gamma, gamma::Ramp}; use crate::{Ferese, backends::direct::gamma, gamma::Ramp};
+8 -2
View File
@@ -7,7 +7,10 @@ use crate::Ferese;
impl InputMethodHandler for Ferese { impl InputMethodHandler for Ferese {
fn new_popup(&mut self, surface: PopupSurface) { 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"); tracing::warn!(?error, "failed to track input method popup");
} }
crate::backends::direct::render_surface(self, surface.wl_surface()); crate::backends::direct::render_surface(self, surface.wl_surface());
@@ -34,7 +37,10 @@ impl InputMethodHandler for Ferese {
return geometry; 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(); return Rectangle::default();
}; };
+56 -22
View File
@@ -1,4 +1,6 @@
use smithay::desktop::{LayerSurface as DesktopLayerSurface, PopupKind, WindowSurfaceType, layer_map_for_output}; use smithay::desktop::{
LayerSurface as DesktopLayerSurface, PopupKind, WindowSurfaceType, layer_map_for_output,
};
use smithay::output::Output; use smithay::output::Output;
use smithay::reexports::wayland_server::protocol::wl_output::WlOutput; use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
@@ -14,7 +16,13 @@ impl WlrLayerShellHandler for Ferese {
&mut self.layer_shell_state &mut self.layer_shell_state
} }
fn new_layer_surface(&mut self, surface: LayerSurface, output: Option<WlOutput>, _layer: Layer, namespace: String) { fn new_layer_surface(
&mut self,
surface: LayerSurface,
output: Option<WlOutput>,
_layer: Layer,
namespace: String,
) {
// A wl_output resource can outlive its registered desktop output. // A wl_output resource can outlive its registered desktop output.
// Explicit requests retain their identity; only unspecified requests // Explicit requests retain their identity; only unspecified requests
// may use the focused/default output policy. // may use the focused/default output policy.
@@ -82,7 +90,11 @@ impl WlrLayerShellHandler for Ferese {
let outputs = self let outputs = self
.space .space
.outputs() .outputs()
.filter(|output| layer_map_for_output(output).layer_geometry(&layer).is_some()) .filter(|output| {
layer_map_for_output(output)
.layer_geometry(&layer)
.is_some()
})
.cloned() .cloned()
.collect::<Vec<_>>(); .collect::<Vec<_>>();
@@ -134,13 +146,20 @@ impl LayoutState {
} }
pub fn handle_commit(state: &mut Ferese, surface: &WlSurface) { 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; return;
}; };
let outputs = state let outputs = state
.space .space
.outputs() .outputs()
.filter(|output| layer_map_for_output(output).layer_geometry(&layer).is_some()) .filter(|output| {
layer_map_for_output(output)
.layer_geometry(&layer)
.is_some()
})
.cloned() .cloned()
.collect::<Vec<_>>(); .collect::<Vec<_>>();
@@ -154,9 +173,9 @@ pub fn handle_commit(state: &mut Ferese, surface: &WlSurface) {
} }
let policy = layer.cached_state(); let policy = layer.cached_state();
let current = LayoutState::current(&layer); let current = LayoutState::current(&layer);
let previous = layer let previous = layer.user_data().get_or_insert_threadsafe(|| {
.user_data() std::sync::Mutex::new(None::<(LayoutState, KeyboardInteractivity)>)
.get_or_insert_threadsafe(|| std::sync::Mutex::new(None::<(LayoutState, KeyboardInteractivity)>)); });
let (changed, requested_focus) = { let (changed, requested_focus) = {
let mut previous = previous.lock().unwrap(); let mut previous = previous.lock().unwrap();
let changed = previous.as_ref().map(|(layout, _)| layout) != Some(&current); let changed = previous.as_ref().map(|(layout, _)| layout) != Some(&current);
@@ -191,7 +210,10 @@ pub fn handle_commit(state: &mut Ferese, surface: &WlSurface) {
}; };
state.reconcile_layer_keyboard_focus(); state.reconcile_layer_keyboard_focus();
if opening_interaction if opening_interaction
&& !state.seat.get_keyboard().is_some_and(|keyboard| keyboard.is_grabbed()) && !state
.seat
.get_keyboard()
.is_some_and(|keyboard| keyboard.is_grabbed())
&& !state.session_lock.active() && !state.session_lock.active()
&& !state.input_capture.captures(1) && !state.input_capture.captures(1)
&& state.layer_keyboard_eligible(&layer) && state.layer_keyboard_eligible(&layer)
@@ -199,11 +221,15 @@ pub fn handle_commit(state: &mut Ferese, surface: &WlSurface) {
&& matches!(policy.layer, Layer::Top | Layer::Overlay) && matches!(policy.layer, Layer::Top | Layer::Overlay)
&& !layer_has_keyboard_focus(state, &layer) && !layer_has_keyboard_focus(state, &layer)
{ {
state.seat.get_keyboard().expect("seat has a keyboard").set_focus( state
state, .seat
Some(layer.wl_surface().clone()), .get_keyboard()
smithay::utils::SERIAL_COUNTER.next_serial(), .expect("seat has a keyboard")
); .set_focus(
state,
Some(layer.wl_surface().clone()),
smithay::utils::SERIAL_COUNTER.next_serial(),
);
} }
if let Err(error) = state.display_handle.flush_clients() { if let Err(error) = state.display_handle.flush_clients() {
@@ -220,9 +246,10 @@ impl Ferese {
/// Use the renderer state updated by on_commit_buffer_handler, as for windows. /// Use the renderer state updated by on_commit_buffer_handler, as for windows.
fn layer_mapped_and_visible(&self, layer: &DesktopLayerSurface) -> bool { fn layer_mapped_and_visible(&self, layer: &DesktopLayerSurface) -> bool {
layer.layer_surface().alive() layer.layer_surface().alive()
&& smithay::backend::renderer::utils::with_renderer_surface_state(layer.wl_surface(), |state| { && smithay::backend::renderer::utils::with_renderer_surface_state(
state.buffer().is_some() layer.wl_surface(),
}) |state| state.buffer().is_some(),
)
.unwrap_or(false) .unwrap_or(false)
&& self.space.outputs().any(|output| { && self.space.outputs().any(|output| {
self.layer_visible_on_output(output, layer.layer()) self.layer_visible_on_output(output, layer.layer())
@@ -239,8 +266,9 @@ impl Ferese {
.find_popup(&surface) .find_popup(&surface)
.and_then(|popup| smithay::desktop::find_popup_root_surface(&popup).ok()) .and_then(|popup| smithay::desktop::find_popup_root_surface(&popup).ok())
.unwrap_or(surface); .unwrap_or(surface);
(smithay::wayland::compositor::get_role(&root) == Some(smithay::wayland::shell::wlr_layer::LAYER_SURFACE_ROLE)) (smithay::wayland::compositor::get_role(&root)
.then_some(root) == Some(smithay::wayland::shell::wlr_layer::LAYER_SURFACE_ROLE))
.then_some(root)
} }
pub(crate) fn keyboard_focused_layer(&self) -> Option<DesktopLayerSurface> { pub(crate) fn keyboard_focused_layer(&self) -> Option<DesktopLayerSurface> {
@@ -255,7 +283,8 @@ impl Ferese {
for priority in [Layer::Overlay, Layer::Top] { for priority in [Layer::Overlay, Layer::Top] {
if let Some(layer) = focused.as_ref().filter(|layer| { if let Some(layer) = focused.as_ref().filter(|layer| {
layer.layer() == priority layer.layer() == priority
&& layer.cached_state().keyboard_interactivity == KeyboardInteractivity::Exclusive && layer.cached_state().keyboard_interactivity
== KeyboardInteractivity::Exclusive
&& self.layer_keyboard_eligible(layer) && self.layer_keyboard_eligible(layer)
}) { }) {
return Some(layer.clone()); return Some(layer.clone());
@@ -313,7 +342,9 @@ impl Ferese {
} }
fn layer_has_keyboard_focus(state: &Ferese, layer: &DesktopLayerSurface) -> bool { fn layer_has_keyboard_focus(state: &Ferese, layer: &DesktopLayerSurface) -> bool {
state.keyboard_focused_layer().is_some_and(|focused| focused == *layer) state
.keyboard_focused_layer()
.is_some_and(|focused| focused == *layer)
} }
#[cfg(test)] #[cfg(test)]
@@ -349,7 +380,10 @@ mod tests {
original, original,
LayoutState::from_policy(policy, Rectangle::from_size((800, 33).into())) LayoutState::from_policy(policy, Rectangle::from_size((800, 33).into()))
); );
assert_ne!(original, LayoutState::from_policy(policy, Rectangle::default())); assert_ne!(
original,
LayoutState::from_policy(policy, Rectangle::default())
);
} }
} }
@@ -151,7 +151,13 @@ impl Client {
) -> Vec<Vec<u32>> { ) -> Vec<Vec<u32>> {
if output == 0 { if output == 0 {
// Reserve the output slot with a spare surface for unspecified requests. // Reserve the output slot with a spare surface for unspecified requests.
request(&mut self.wire, self.compositor.id().protocol_id(), 0, &[5], None); request(
&mut self.wire,
self.compositor.id().protocol_id(),
0,
&[5],
None,
);
} }
request( request(
&mut self.wire, &mut self.wire,
@@ -199,8 +205,9 @@ fn assert_closed_without_configure(messages: &[Vec<u32>]) {
#[test] #[test]
fn zero_outputs_close_new_layer_roles() { fn zero_outputs_close_new_layer_roles() {
if !crate::startup_tests::private_runtime("handlers::layer_shell::output_tests::zero_outputs_close_new_layer_roles") if !crate::startup_tests::private_runtime(
{ "handlers::layer_shell::output_tests::zero_outputs_close_new_layer_roles",
) {
return; return;
} }
let (mut state, mut events) = fixture(); let (mut state, mut events) = fixture();
@@ -259,8 +266,9 @@ fn unspecified_output_uses_focused_registered_output() {
#[test] #[test]
fn valid_explicit_output_is_preserved() { fn valid_explicit_output_is_preserved() {
if !crate::startup_tests::private_runtime("handlers::layer_shell::output_tests::valid_explicit_output_is_preserved") if !crate::startup_tests::private_runtime(
{ "handlers::layer_shell::output_tests::valid_explicit_output_is_preserved",
) {
return; return;
} }
let (mut state, mut events) = fixture(); let (mut state, mut events) = fixture();
+43 -13
View File
@@ -28,7 +28,9 @@ use smithay::wayland::keyboard_shortcuts_inhibit::{
KeyboardShortcutsInhibitorSeat, KeyboardShortcutsInhibitorSeat,
}; };
use smithay::wayland::output::OutputHandler; use smithay::wayland::output::OutputHandler;
use smithay::wayland::pointer_constraints::{PointerConstraint, PointerConstraintsHandler, with_pointer_constraint}; use smithay::wayland::pointer_constraints::{
PointerConstraint, PointerConstraintsHandler, with_pointer_constraint,
};
use smithay::wayland::selection::SelectionHandler; use smithay::wayland::selection::SelectionHandler;
use smithay::wayland::selection::data_device::{ use smithay::wayland::selection::data_device::{
DataDeviceHandler, DataDeviceState, WaylandDndGrabHandler, set_data_device_focus, DataDeviceHandler, DataDeviceState, WaylandDndGrabHandler, set_data_device_focus,
@@ -54,12 +56,17 @@ impl SeatHandler for Ferese {
if let CursorImageStatus::Named(icon) = &image if let CursorImageStatus::Named(icon) = &image
&& !self.named_cursors.contains_key(icon) && !self.named_cursors.contains_key(icon)
{ {
self.cursor_loader.request(*icon, self.cursor_animation.buffer_scale); self.cursor_loader
.request(*icon, self.cursor_animation.buffer_scale);
} }
self.cursor_animation.reset |= match (&self.cursor_status, &image) { self.cursor_animation.reset |= match (&self.cursor_status, &image) {
(CursorImageStatus::Named(previous), CursorImageStatus::Named(next)) => previous != next, (CursorImageStatus::Named(previous), CursorImageStatus::Named(next)) => {
previous != next
}
(CursorImageStatus::Hidden, CursorImageStatus::Hidden) => false, (CursorImageStatus::Hidden, CursorImageStatus::Hidden) => false,
(CursorImageStatus::Surface(previous), CursorImageStatus::Surface(next)) => previous != next, (CursorImageStatus::Surface(previous), CursorImageStatus::Surface(next)) => {
previous != next
}
_ => true, _ => true,
}; };
self.cursor_status = image; self.cursor_status = image;
@@ -74,7 +81,10 @@ impl SeatHandler for Ferese {
self.pending_layer_focus = None; self.pending_layer_focus = None;
// Smithay has already sent enter. last_enter uses a separate mutex; // Smithay has already sent enter. last_enter uses a separate mutex;
// querying current_focus here would reenter the held keyboard lock. // querying current_focus here would reenter the held keyboard lock.
if let Some(serial) = seat.get_keyboard().and_then(|keyboard| keyboard.last_enter()) { if let Some(serial) = seat
.get_keyboard()
.and_then(|keyboard| keyboard.last_enter())
{
self.record_activation_input(serial, focused.cloned()); self.record_activation_input(serial, focused.cloned());
} }
@@ -86,7 +96,9 @@ impl SeatHandler for Ferese {
{ {
inhibitor.inactivate(); 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(); inhibitor.activate();
self.active_shortcuts_inhibitor = Some(inhibitor); self.active_shortcuts_inhibitor = Some(inhibitor);
} }
@@ -136,7 +148,10 @@ impl KeyboardShortcutsInhibitHandler for Ferese {
} }
fn new_inhibitor(&mut self, inhibitor: KeyboardShortcutsInhibitor) { 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()) { if focused.as_ref() == Some(inhibitor.wl_surface()) {
inhibitor.activate(); inhibitor.activate();
@@ -216,7 +231,9 @@ impl Ferese {
let output = output let output = output
.or_else(|| self.focused_output()) .or_else(|| self.focused_output())
.or_else(|| self.space.outputs().next()); .or_else(|| self.space.outputs().next());
with_states(surface, |states| set_surface_preferences(surface, states, output)); with_states(surface, |states| {
set_surface_preferences(surface, states, output)
});
} }
} }
@@ -226,9 +243,18 @@ fn set_surface_preferences(
output: Option<&smithay::output::Output>, output: Option<&smithay::output::Output>,
) { ) {
let scale = output.map_or(1.0, |output| output.current_scale().fractional_scale()); let scale = output.map_or(1.0, |output| output.current_scale().fractional_scale());
let transform = output.map_or(smithay::utils::Transform::Normal, |output| output.current_transform()); let transform = output.map_or(smithay::utils::Transform::Normal, |output| {
smithay::wayland::compositor::send_surface_state(surface, states, scale.ceil() as i32, transform); output.current_transform()
with_fractional_scale(states, |surface_scale| surface_scale.set_preferred_scale(scale)); });
smithay::wayland::compositor::send_surface_state(
surface,
states,
scale.ceil() as i32,
transform,
);
with_fractional_scale(states, |surface_scale| {
surface_scale.set_preferred_scale(scale)
});
} }
pub(crate) fn set_surface_tree_output(surface: &WlSurface, output: &smithay::output::Output) { pub(crate) fn set_surface_tree_output(surface: &WlSurface, output: &smithay::output::Output) {
@@ -337,13 +363,17 @@ impl PrimarySelectionHandler for Ferese {
} }
impl smithay::wayland::selection::wlr_data_control::DataControlHandler for Ferese { impl smithay::wayland::selection::wlr_data_control::DataControlHandler for Ferese {
fn data_control_state(&mut self) -> &mut smithay::wayland::selection::wlr_data_control::DataControlState { fn data_control_state(
&mut self,
) -> &mut smithay::wayland::selection::wlr_data_control::DataControlState {
&mut self.wlr_data_control_state &mut self.wlr_data_control_state
} }
} }
impl smithay::wayland::selection::ext_data_control::DataControlHandler for Ferese { impl smithay::wayland::selection::ext_data_control::DataControlHandler for Ferese {
fn data_control_state(&mut self) -> &mut smithay::wayland::selection::ext_data_control::DataControlState { fn data_control_state(
&mut self,
) -> &mut smithay::wayland::selection::ext_data_control::DataControlState {
&mut self.ext_data_control_state &mut self.ext_data_control_state
} }
} }
+37 -14
View File
@@ -11,7 +11,9 @@ use smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::{
}; };
use smithay::reexports::wayland_server::protocol::wl_buffer::WlBuffer; use smithay::reexports::wayland_server::protocol::wl_buffer::WlBuffer;
use smithay::reexports::wayland_server::protocol::wl_shm; use smithay::reexports::wayland_server::protocol::wl_shm;
use smithay::reexports::wayland_server::{Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource}; use smithay::reexports::wayland_server::{
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
};
use smithay::utils::{Buffer, Logical, Rectangle, Size}; use smithay::utils::{Buffer, Logical, Rectangle, Size};
use smithay::wayland::shm::with_buffer_contents_mut; use smithay::wayland::shm::with_buffer_contents_mut;
@@ -157,7 +159,8 @@ impl PendingScreencopy {
pub(crate) fn capture_allowed() -> bool { pub(crate) fn capture_allowed() -> bool {
static ALLOWED: std::sync::OnceLock<bool> = std::sync::OnceLock::new(); 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 { fn capture_opted_in(value: Option<&std::ffi::OsStr>) -> bool {
@@ -321,7 +324,9 @@ impl Ferese {
let captures = std::mem::take(&mut self.pending_screencopies); let captures = std::mem::take(&mut self.pending_screencopies);
for capture in captures { 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); remaining.push(capture);
continue; continue;
} }
@@ -332,7 +337,8 @@ impl Ferese {
} }
framebuffer_binding_changed = true; 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(); capture.fail();
} }
} }
@@ -342,15 +348,18 @@ impl Ferese {
} }
pub(crate) fn has_pending_screencopy(&self, output: &Output, overlay_cursor: bool) -> bool { pub(crate) fn has_pending_screencopy(&self, output: &Output, overlay_cursor: bool) -> bool {
self.pending_screencopies self.pending_screencopies.iter().any(|capture| {
.iter() capture.output == *output
.any(|capture| capture.output == *output && cursor_overlay_matches(capture.overlay_cursor, overlay_cursor)) && cursor_overlay_matches(capture.overlay_cursor, overlay_cursor)
})
} }
pub(crate) fn fail_screencopies(&mut self, output: &Output, overlay_cursor: bool) { pub(crate) fn fail_screencopies(&mut self, output: &Output, overlay_cursor: bool) {
let mut remaining = Vec::new(); let mut remaining = Vec::new();
for capture in std::mem::take(&mut self.pending_screencopies) { for capture in std::mem::take(&mut self.pending_screencopies) {
if capture.output == *output && cursor_overlay_matches(capture.overlay_cursor, overlay_cursor) { if capture.output == *output
&& cursor_overlay_matches(capture.overlay_cursor, overlay_cursor)
{
capture.sink.fail(); capture.sink.fail();
} else { } else {
remaining.push(capture); remaining.push(capture);
@@ -422,7 +431,10 @@ fn create_frame(
} }
} }
fn capture_region(output: &Output, requested: Option<Rectangle<i32, Logical>>) -> Option<Rectangle<i32, Buffer>> { fn capture_region(
output: &Output,
requested: Option<Rectangle<i32, Logical>>,
) -> Option<Rectangle<i32, Buffer>> {
let mode = output.current_mode()?; let mode = output.current_mode()?;
let scale = output.current_scale().fractional_scale(); let scale = output.current_scale().fractional_scale();
let transform = output.current_transform(); let transform = output.current_transform();
@@ -458,7 +470,9 @@ pub(crate) fn capture_region_for_geometry(
} }
let logical_size = output_logical_size(mode_size, scale, transform); let logical_size = output_logical_size(mode_size, scale, transform);
let output_region = Rectangle::from_size(logical_size); 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 { if logical_region.size.w <= 0 || logical_region.size.h <= 0 {
return None; return None;
@@ -509,7 +523,9 @@ where
tracing::warn!(?error, "failed to map output framebuffer copy"); tracing::warn!(?error, "failed to map output framebuffer copy");
})?; })?;
let expected = capture.region.size; 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(())?; let bytes = stride.checked_mul(expected.h as usize).ok_or(())?;
if source.len() < bytes { if source.len() < bytes {
return Err(()); return Err(());
@@ -567,7 +583,11 @@ where
frame.flags(zwlr_screencopy_frame_v1::Flags::empty()); frame.flags(zwlr_screencopy_frame_v1::Flags::empty());
let seconds = timestamp.as_secs(); 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(()) Ok(())
} }
@@ -596,7 +616,8 @@ mod tests {
let mode = Size::<i32, Physical>::from((1_920, 1_080)); let mode = Size::<i32, Physical>::from((1_920, 1_080));
let requested = Rectangle::new((-10, 20).into(), (110, 50).into()); 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())); assert_eq!(region, Rectangle::new((0, 40).into(), (200, 100).into()));
} }
@@ -606,7 +627,9 @@ mod tests {
let mode = Size::<i32, Physical>::from((1_920, 1_080)); let mode = Size::<i32, Physical>::from((1_920, 1_080));
let requested = Rectangle::new((1_000, 600).into(), (100, 100).into()); 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] #[test]
+341 -81
View File
@@ -40,7 +40,9 @@ const MAX_CANVAS_BYTES: usize = 128 * 1024 * 1024;
impl Canvas { impl Canvas {
fn new(width: u32, height: u32) -> Option<Self> { 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 { if width == 0 || height == 0 || length > MAX_CANVAS_BYTES {
return None; return None;
} }
@@ -83,7 +85,11 @@ impl Canvas {
} }
} }
fn scale_and_convert(frame: &OutputFrame, target_width: usize, target_height: usize) -> Option<Vec<u8>> { fn scale_and_convert(
frame: &OutputFrame,
target_width: usize,
target_height: usize,
) -> Option<Vec<u8>> {
let (upright_width, upright_height) = upright_size(frame); let (upright_width, upright_height) = upright_size(frame);
if upright_width <= 0 || upright_height <= 0 || target_width == 0 || target_height == 0 { if upright_width <= 0 || upright_height <= 0 || target_width == 0 || target_height == 0 {
return None; return None;
@@ -95,8 +101,13 @@ fn scale_and_convert(frame: &OutputFrame, target_width: usize, target_height: us
if upright_width as usize == target_width && upright_height as usize == target_height { if upright_width as usize == target_width && upright_height as usize == target_height {
for row in 0..target_height { for row in 0..target_height {
for column in 0..target_width { for column in 0..target_width {
let (x, y) = let (x, y) = upright_source_pixel(
upright_source_pixel(column as i32, row as i32, frame.width, frame.height, frame.transform); column as i32,
row as i32,
frame.width,
frame.height,
frame.transform,
);
let from = (y as usize) let from = (y as usize)
.checked_mul(frame.stride)? .checked_mul(frame.stride)?
.checked_add(x as usize * BYTES_PER_PIXEL)?; .checked_add(x as usize * BYTES_PER_PIXEL)?;
@@ -147,7 +158,13 @@ fn scale_and_convert(frame: &OutputFrame, target_width: usize, target_height: us
Some(out) 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 { match transform {
Transform::_90 => (y, height - 1 - x), Transform::_90 => (y, height - 1 - x),
Transform::_180 => (width - 1 - x, height - 1 - y), Transform::_180 => (width - 1 - x, height - 1 - y),
@@ -173,7 +190,10 @@ pub(crate) fn compose(frames: &[OutputFrame]) -> Result<Canvas, String> {
} }
let (left, top) = logical_origin(frames).ok_or("Screenshot region is empty")?; let (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) { if !(scale.is_finite() && scale > 0.0) {
return Err("Screenshot outputs have an invalid scale".into()); return Err("Screenshot outputs have an invalid scale".into());
} }
@@ -200,15 +220,22 @@ pub(crate) fn compose(frames: &[OutputFrame]) -> Result<Canvas, String> {
if width <= 0 || height <= 0 { if width <= 0 || height <= 0 {
return Err("Screenshot region is empty".into()); 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) { for (frame, (x0, y0, target_width, target_height)) in frames.iter().zip(placement) {
if target_width == 0 || target_height == 0 { if target_width == 0 || target_height == 0 {
continue; continue;
} }
let scaled = let scaled = scale_and_convert(frame, target_width as usize, target_height as usize)
scale_and_convert(frame, target_width as usize, target_height as usize).ok_or("Invalid capture size")?; .ok_or("Invalid capture size")?;
canvas.blit(&scaled, target_width as usize, target_height as usize, x0, y0); canvas.blit(
&scaled,
target_width as usize,
target_height as usize,
x0,
y0,
);
} }
Ok(canvas) Ok(canvas)
@@ -269,7 +296,12 @@ fn logical_origin(frames: &[OutputFrame]) -> Option<(i32, i32)> {
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Geometry { pub(crate) enum Geometry {
All, All,
Region { x: i32, y: i32, width: i32, height: i32 }, Region {
x: i32,
y: i32,
width: i32,
height: i32,
},
} }
impl Geometry { impl Geometry {
@@ -279,9 +311,16 @@ impl Geometry {
if width <= 0 || height <= 0 { if width <= 0 || height <= 0 {
return Err("Screenshot region must have a positive size".into()); return Err("Screenshot region must have a positive size".into());
} }
x.checked_add(width).ok_or("screenshot geometry overflows")?; x.checked_add(width)
y.checked_add(height).ok_or("screenshot geometry overflows")?; .ok_or("screenshot geometry overflows")?;
Ok(Geometry::Region { x, y, width, height }) y.checked_add(height)
.ok_or("screenshot geometry overflows")?;
Ok(Geometry::Region {
x,
y,
width,
height,
})
} }
} }
@@ -402,7 +441,8 @@ impl Budget {
fn reserve(self: &Arc<Self>, bytes: usize) -> Result<BudgetPermit, String> { fn reserve(self: &Arc<Self>, bytes: usize) -> Result<BudgetPermit, String> {
self.used self.used
.fetch_update(Ordering::AcqRel, Ordering::Acquire, |used| { .fetch_update(Ordering::AcqRel, Ordering::Acquire, |used| {
used.checked_add(bytes).filter(|total| *total <= MAX_PENDING_BYTES) used.checked_add(bytes)
.filter(|total| *total <= MAX_PENDING_BYTES)
}) })
.map_err(|_| "Screenshot memory budget is exhausted".to_string())?; .map_err(|_| "Screenshot memory budget is exhausted".to_string())?;
Ok(BudgetPermit { Ok(BudgetPermit {
@@ -492,7 +532,10 @@ impl Coordinator {
response_id, response_id,
reply, reply,
state: RequestState::Collecting, 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, received: 0,
deadline: now + REQUEST_TIMEOUT, deadline: now + REQUEST_TIMEOUT,
permit: Some(permit), permit: Some(permit),
@@ -503,7 +546,9 @@ impl Coordinator {
} }
pub(crate) fn permit(&self, request: u64) -> Option<BudgetPermit> { pub(crate) fn permit(&self, request: u64) -> Option<BudgetPermit> {
self.requests.get(&request).and_then(|entry| entry.permit.clone()) self.requests
.get(&request)
.and_then(|entry| entry.permit.clone())
} }
// Used by the completion tests to assert that a request is still tracked. // Used by the completion tests to assert that a request is still tracked.
@@ -519,7 +564,12 @@ impl Coordinator {
) )
} }
pub(crate) fn on_part(&mut self, request: u64, part: usize, result: Result<CaptureBuffer, String>) -> Action { pub(crate) fn on_part(
&mut self,
request: u64,
part: usize,
result: Result<CaptureBuffer, String>,
) -> Action {
let Some(entry) = self.requests.get_mut(&request) else { let Some(entry) = self.requests.get_mut(&request) else {
return Action::None; return Action::None;
}; };
@@ -572,7 +622,10 @@ impl Coordinator {
Action::Encode { Action::Encode {
request, request,
frames, frames,
permit: entry.permit.take().expect("collecting request owns its reservation"), permit: entry
.permit
.take()
.expect("collecting request owns its reservation"),
} }
} }
@@ -613,7 +666,10 @@ impl Coordinator {
.map(|(id, _)| *id) .map(|(id, _)| *id)
.collect(); .collect();
for id in &expired { 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 expired
} }
@@ -728,7 +784,10 @@ impl PlannedPart {
// Turns a requested logical region into the per-output reads that cover it. // 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 // The logical-to-buffer conversion is shared with the screencopy path so both
// agree on rounding at fractional scales. // 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() { if outputs.is_empty() {
return Err("No outputs are enabled".into()); return Err("No outputs are enabled".into());
} }
@@ -739,7 +798,8 @@ pub(crate) fn plan(geometry: &Geometry, outputs: &[OutputLayout]) -> Result<Vec<
{ {
return Err("Screenshot region must have a positive size".into()); 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 { let requested = match geometry {
Geometry::All => { Geometry::All => {
@@ -751,8 +811,10 @@ pub(crate) fn plan(geometry: &Geometry, outputs: &[OutputLayout]) -> Result<Vec<
}; };
union = Some(match union { union = Some(match union {
Some(current) => { Some(current) => {
let top_left = let top_left = Point::<i32, Logical>::new(
Point::<i32, Logical>::new(current.loc.x.min(rect.loc.x), current.loc.y.min(rect.loc.y)); current.loc.x.min(rect.loc.x),
current.loc.y.min(rect.loc.y),
);
let bottom_right = Point::<i32, Logical>::new( let bottom_right = Point::<i32, Logical>::new(
(current.loc.x + current.size.w).max(rect.loc.x + rect.size.w), (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), (current.loc.y + current.size.h).max(rect.loc.y + rect.size.h),
@@ -764,7 +826,12 @@ pub(crate) fn plan(geometry: &Geometry, outputs: &[OutputLayout]) -> Result<Vec<
} }
union.expect("outputs is not empty") union.expect("outputs is not empty")
} }
Geometry::Region { x, y, width, height } => Rectangle { Geometry::Region {
x,
y,
width,
height,
} => Rectangle {
loc: (*x, *y).into(), loc: (*x, *y).into(),
size: (*width, *height).into(), size: (*width, *height).into(),
}, },
@@ -792,7 +859,11 @@ pub(crate) fn plan(geometry: &Geometry, outputs: &[OutputLayout]) -> Result<Vec<
} }
// capture_region_for_geometry works in output-local logical space. // capture_region_for_geometry works in output-local logical space.
let local = Rectangle { 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, size: clipped.size,
}; };
// The clip above already intersected against this same logical rect, so // The clip above already intersected against this same logical rect, so
@@ -839,19 +910,34 @@ mod planning {
} }
fn region(x: i32, y: i32, width: i32, height: i32) -> Geometry { fn region(x: i32, y: i32, width: i32, height: i32) -> Geometry {
Geometry::Region { x, y, width, height } Geometry::Region {
x,
y,
width,
height,
}
} }
#[test] #[test]
fn a_crop_crossing_a_seam_splits_into_one_part_per_output() { 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(); let parts = plan(&region(1800, 0, 200, 100), &outputs).unwrap();
assert_eq!(parts.len(), 2); assert_eq!(parts.len(), 2);
assert_eq!(parts[0].index, 0); assert_eq!(parts[0].index, 0);
assert_eq!((parts[0].logical.loc.x, parts[0].logical.size.w), (1800, 120));
assert_eq!( assert_eq!(
(parts[1].index, parts[1].logical.loc.x, parts[1].logical.size.w), (parts[0].logical.loc.x, parts[0].logical.size.w),
(1800, 120)
);
assert_eq!(
(
parts[1].index,
parts[1].logical.loc.x,
parts[1].logical.size.w
),
(1, 1920, 80) (1, 1920, 80)
); );
// The clipped widths must add back up to the request. // The clipped widths must add back up to the request.
@@ -884,8 +970,14 @@ mod planning {
let outputs = [output(0, 0, 800, 600, 1.0), output(800, 0, 1600, 1200, 1.5)]; let 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(); let parts = plan(&region(600, 0, 400, 300), &outputs).unwrap();
assert_eq!(parts.len(), 2); assert_eq!(parts.len(), 2);
assert_eq!((parts[0].logical.loc.x, parts[0].logical.size.w), (600, 200)); assert_eq!(
assert_eq!((parts[1].logical.loc.x, parts[1].logical.size.w), (800, 200)); (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. // The 1.5 output reads back at 1.5x while still reporting logical 200.
let spec = parts[1].spec(&outputs[1]); let spec = parts[1].spec(&outputs[1]);
assert_eq!(spec.logical_width, 200); assert_eq!(spec.logical_width, 200);
@@ -895,7 +987,10 @@ mod planning {
#[test] #[test]
fn all_geometry_unions_every_output() { 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(); let parts = plan(&Geometry::All, &outputs).unwrap();
assert_eq!(parts.len(), 2); assert_eq!(parts.len(), 2);
// Neither output is moved into a normalised origin at plan time. // Neither output is moved into a normalised origin at plan time.
@@ -906,7 +1001,10 @@ mod planning {
#[test] #[test]
fn a_crop_misses_outputs_that_do_not_intersect() { 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); let parts = plan(&region(4000, 0, 100, 100), &outputs);
assert!(parts.is_err(), "a region off the desktop is reported"); assert!(parts.is_err(), "a region off the desktop is reported");
} }
@@ -916,11 +1014,17 @@ mod planning {
// The second output starts exactly where the region ends. It contributes // The second output starts exactly where the region ends. It contributes
// nothing, so the capture must still succeed with just the first output // nothing, so the capture must still succeed with just the first output
// rather than failing on a region it does not overlap. // 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"); 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.len(), 1, "only the overlapping output is read");
assert_eq!(parts[0].index, 0); 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] #[test]
@@ -930,10 +1034,16 @@ mod planning {
// A 90-degree transform turns the 1920x1080 mode into a 1080x1920 // A 90-degree transform turns the 1920x1080 mode into a 1080x1920
// logical area, and the readback follows the same rotation. // logical area, and the readback follows the same rotation.
let parts = plan(&region(0, 0, 1080, 400), std::slice::from_ref(&rotated)).unwrap(); 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); let spec = parts[0].spec(&rotated);
assert_eq!(spec.transform, Transform::_90); 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] #[test]
@@ -1013,9 +1123,17 @@ mod completion {
let (reply, received) = sync_channel(8); let (reply, received) = sync_channel(8);
let mut coordinator = Coordinator::new(); let mut coordinator = Coordinator::new();
coordinator coordinator
.admit(1, 42, reply.clone(), (0..parts).map(|_| spec(2, 2)).collect()) .admit(
1,
42,
reply.clone(),
(0..parts).map(|_| spec(2, 2)).collect(),
)
.expect("admitted"); .expect("admitted");
Self { coordinator, received } Self {
coordinator,
received,
}
} }
fn response(&self) -> Option<Response> { fn response(&self) -> Option<Response> {
@@ -1036,7 +1154,11 @@ mod completion {
}; };
assert_eq!(frames.len(), 1); assert_eq!(frames.len(), 1);
assert_eq!(frames[0].pixels.as_ptr(), allocation, "readback pixels were copied"); assert_eq!(
frames[0].pixels.as_ptr(),
allocation,
"readback pixels were copied"
);
assert_eq!(frames[0].pixels, vec![137; 16]); assert_eq!(frames[0].pixels, vec![137; 16]);
} }
@@ -1049,13 +1171,21 @@ mod completion {
harness.coordinator.on_part(id, 0, Ok(buffer(2, 2))), harness.coordinator.on_part(id, 0, Ok(buffer(2, 2))),
Action::None 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!( assert!(matches!(
harness.coordinator.on_part(id, 1, Err("readback failed".into())), harness
.coordinator
.on_part(id, 1, Err("readback failed".into())),
Action::None 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"); let response = harness.response().expect("caller is always answered");
assert_eq!(response.id, 42); assert_eq!(response.id, 42);
@@ -1070,7 +1200,9 @@ mod completion {
harness.coordinator.on_part(id, 0, Ok(buffer(2, 2))); harness.coordinator.on_part(id, 0, Ok(buffer(2, 2)));
// Output 1 is removed from the layout, so its pending capture is failed. // 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.coordinator.is_live(id));
assert!(harness.response().unwrap().error.is_some()); assert!(harness.response().unwrap().error.is_some());
@@ -1109,14 +1241,26 @@ mod completion {
fn admission_enforces_one_budget_across_requests_and_late_readbacks() { fn admission_enforces_one_budget_across_requests_and_late_readbacks() {
let (reply, _received) = sync_channel(8); let (reply, _received) = sync_channel(8);
let mut coordinator = Coordinator::new(); let mut coordinator = Coordinator::new();
let first = coordinator.admit(1, 1, reply.clone(), vec![spec(4096, 8192)]).unwrap(); let first = coordinator
let second = coordinator.admit(1, 2, reply.clone(), vec![spec(4096, 8192)]).unwrap(); .admit(1, 1, reply.clone(), vec![spec(4096, 8192)])
assert!(coordinator.admit(1, 3, reply.clone(), vec![spec(2, 2)]).is_err()); .unwrap();
let second = coordinator
.admit(1, 2, reply.clone(), vec![spec(4096, 8192)])
.unwrap();
assert!(
coordinator
.admit(1, 3, reply.clone(), vec![spec(2, 2)])
.is_err()
);
// A cancelled request can still have a readback retained by a sink or channel. // A cancelled request can still have a readback retained by a sink or channel.
let late_readback = coordinator.permit(first).unwrap(); let late_readback = coordinator.permit(first).unwrap();
coordinator.reject(first, "cancelled"); coordinator.reject(first, "cancelled");
assert!(coordinator.admit(1, 4, reply.clone(), vec![spec(2, 2)]).is_err()); assert!(
coordinator
.admit(1, 4, reply.clone(), vec![spec(2, 2)])
.is_err()
);
drop(late_readback); drop(late_readback);
assert!(coordinator.admit(1, 5, reply, vec![spec(2, 2)]).is_ok()); assert!(coordinator.admit(1, 5, reply, vec![spec(2, 2)]).is_ok());
coordinator.reject(second, "finished"); coordinator.reject(second, "finished");
@@ -1127,19 +1271,48 @@ mod completion {
let mut harness = Harness::new(1); let mut harness = Harness::new(1);
let id = harness.coordinator.next_id; let id = harness.coordinator.next_id;
let bytes = harness.coordinator.budget.used.load(Ordering::Acquire); let bytes = harness.coordinator.budget.used.load(Ordering::Acquire);
let Action::Encode { frames, permit, .. } = harness.coordinator.on_part(id, 0, Ok(buffer(2, 2))) else { let Action::Encode { frames, permit, .. } =
harness.coordinator.on_part(id, 0, Ok(buffer(2, 2)))
else {
panic!("ready request should encode"); panic!("ready request should encode");
}; };
harness.coordinator.reject(id, "cancelled while encoder owns pixels"); harness
assert_eq!(harness.coordinator.budget.used.load(Ordering::Acquire), bytes); .coordinator
assert!(harness.coordinator.budget.reserve(MAX_PENDING_BYTES).is_err()); .reject(id, "cancelled while encoder owns pixels");
assert_eq!(
harness.coordinator.budget.used.load(Ordering::Acquire),
bytes
);
assert!(
harness
.coordinator
.budget
.reserve(MAX_PENDING_BYTES)
.is_err()
);
drop(frames); drop(frames);
drop(permit); drop(permit);
assert_eq!(harness.coordinator.budget.used.load(Ordering::Acquire), 0); assert_eq!(harness.coordinator.budget.used.load(Ordering::Acquire), 0);
let first = harness.coordinator.budget.reserve(MAX_PENDING_BYTES / 2 + 1).unwrap(); let first = harness
assert!(harness.coordinator.budget.reserve(MAX_PENDING_BYTES / 2).is_err()); .coordinator
.budget
.reserve(MAX_PENDING_BYTES / 2 + 1)
.unwrap();
assert!(
harness
.coordinator
.budget
.reserve(MAX_PENDING_BYTES / 2)
.is_err()
);
drop(first); drop(first);
assert!(harness.coordinator.budget.reserve(MAX_PENDING_BYTES).is_ok()); assert!(
harness
.coordinator
.budget
.reserve(MAX_PENDING_BYTES)
.is_ok()
);
} }
#[test] #[test]
@@ -1152,7 +1325,10 @@ mod completion {
Action::None Action::None
)); ));
let action = harness.coordinator.on_part(id, 1, Ok(buffer(2, 2))); let action = harness.coordinator.on_part(id, 1, Ok(buffer(2, 2)));
let Action::Encode { request, frames, .. } = action else { let Action::Encode {
request, frames, ..
} = action
else {
panic!("expected the request to move to encoding"); panic!("expected the request to move to encoding");
}; };
assert_eq!(request, id); assert_eq!(request, id);
@@ -1177,7 +1353,9 @@ mod completion {
// Unlocking does not resurrect the request; the file the worker // Unlocking does not resurrect the request; the file the worker
// eventually produces is ours to delete. // eventually produces is ours to delete.
assert!(!harness.coordinator.is_live(id)); 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!(matches!(action, Action::DiscardFile(ref path) if path.ends_with("late.png")));
assert!(harness.response().is_none(), "no late success is delivered"); assert!(harness.response().is_none(), "no late success is delivered");
} }
@@ -1189,7 +1367,9 @@ mod completion {
harness.coordinator.on_part(id, 0, Ok(buffer(2, 2))); harness.coordinator.on_part(id, 0, Ok(buffer(2, 2)));
assert!(harness.coordinator.is_encoding(id)); assert!(harness.coordinator.is_encoding(id));
assert_eq!( assert_eq!(
harness.coordinator.terminate_all_with_reason("Capture privacy changed"), harness
.coordinator
.terminate_all_with_reason("Capture privacy changed"),
vec![id] vec![id]
); );
assert!(harness.response().unwrap().error.is_some()); assert!(harness.response().unwrap().error.is_some());
@@ -1208,7 +1388,9 @@ mod completion {
let id = harness.coordinator.next_id; let id = harness.coordinator.next_id;
harness.coordinator.on_part(id, 0, Ok(buffer(2, 2))); 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"); let response = harness.response().expect("success is answered");
assert_eq!(response.id, 42); assert_eq!(response.id, 42);
assert!(response.error.is_none()); assert!(response.error.is_none());
@@ -1216,7 +1398,9 @@ mod completion {
// A duplicate completion for the same request has nowhere to go, and // A duplicate completion for the same request has nowhere to go, and
// its file is an orphan rather than a second delivery. // 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(_))); assert!(matches!(action, Action::DiscardFile(_)));
// A delivered path is reported separately so the caller cannot // A delivered path is reported separately so the caller cannot
@@ -1239,7 +1423,9 @@ mod completion {
let id = harness.coordinator.next_id; let id = harness.coordinator.next_id;
harness.coordinator.on_part(id, 0, Ok(buffer(2, 2))); 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"); let response = harness.response().expect("failure is answered");
assert_eq!(response.error.unwrap().message, "encoding failed"); assert_eq!(response.error.unwrap().message, "encoding failed");
assert!(!harness.coordinator.is_live(id)); assert!(!harness.coordinator.is_live(id));
@@ -1252,13 +1438,23 @@ mod completion {
// client would never be answered. // client would never be answered.
let mut harness = Harness::new(2); let mut harness = Harness::new(2);
let id = harness.coordinator.next_id; let id = harness.coordinator.next_id;
harness.coordinator.requests.get_mut(&id).expect("tracked").deadline = Instant::now() - Duration::from_secs(1); harness
.coordinator
.requests
.get_mut(&id)
.expect("tracked")
.deadline = Instant::now() - Duration::from_secs(1);
assert_eq!(harness.coordinator.expire_at(Instant::now()), vec![id]); assert_eq!(harness.coordinator.expire_at(Instant::now()), vec![id]);
let response = harness.response().expect("the caller is answered"); let response = harness.response().expect("the caller is answered");
assert_eq!(response.error.as_ref().unwrap().code, "screenshot_failed"); assert_eq!(response.error.as_ref().unwrap().code, "screenshot_failed");
assert!( assert!(
response.error.as_ref().unwrap().message.contains("timed out"), response
.error
.as_ref()
.unwrap()
.message
.contains("timed out"),
"the failure explains itself: {:?}", "the failure explains itself: {:?}",
response.error.as_ref().unwrap().message response.error.as_ref().unwrap().message
); );
@@ -1275,7 +1471,10 @@ mod completion {
fn a_live_request_is_not_expired_early() { fn a_live_request_is_not_expired_early() {
let mut harness = Harness::new(1); let mut harness = Harness::new(1);
let id = harness.coordinator.next_id; let id = harness.coordinator.next_id;
assert!(harness.coordinator.expire_at(Instant::now()).is_empty(), "not yet due"); assert!(
harness.coordinator.expire_at(Instant::now()).is_empty(),
"not yet due"
);
assert!(harness.coordinator.is_live(id)); assert!(harness.coordinator.is_live(id));
assert!(harness.response().is_none(), "still unanswered, not failed"); assert!(harness.response().is_none(), "still unanswered, not failed");
let remaining = harness let remaining = harness
@@ -1334,7 +1533,9 @@ mod completion {
); );
} }
fn observe_deadlines(coordinator: &mut Coordinator) -> std::rc::Rc<std::cell::RefCell<Vec<Option<Instant>>>> { fn observe_deadlines(
coordinator: &mut Coordinator,
) -> std::rc::Rc<std::cell::RefCell<Vec<Option<Instant>>>> {
let changes = std::rc::Rc::new(std::cell::RefCell::new(Vec::new())); let changes = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
let observed = changes.clone(); let observed = changes.clone();
coordinator.set_deadline_observer(move |deadline| observed.borrow_mut().push(deadline)); coordinator.set_deadline_observer(move |deadline| observed.borrow_mut().push(deadline));
@@ -1356,15 +1557,29 @@ mod completion {
.admit_at(1, 42, reply.clone(), vec![spec(2, 2)], late) .admit_at(1, 42, reply.clone(), vec![spec(2, 2)], late)
.unwrap(); .unwrap();
let second = coordinator let second = coordinator
.admit_at(2, 43, reply.clone(), vec![spec(2, 2)], late + Duration::from_secs(5)) .admit_at(
2,
43,
reply.clone(),
vec![spec(2, 2)],
late + Duration::from_secs(5),
)
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
changes.borrow().len(), changes.borrow().len(),
2, 2,
"a later request does not rearm the earliest timer" "a later request does not rearm the earliest timer"
); );
assert!(coordinator.admit_at(1, 44, reply, Vec::new(), late).is_err()); assert!(
assert_eq!(changes.borrow().len(), 2, "rejected admission does not touch the timer"); coordinator
.admit_at(1, 44, reply, Vec::new(), late)
.is_err()
);
assert_eq!(
changes.borrow().len(),
2,
"rejected admission does not touch the timer"
);
assert_eq!(coordinator.terminate_owner(1), vec![first]); assert_eq!(coordinator.terminate_owner(1), vec![first]);
assert!(coordinator.is_live(second)); assert!(coordinator.is_live(second));
assert_eq!(coordinator.terminate_owner(99), Vec::<u64>::new()); assert_eq!(coordinator.terminate_owner(99), Vec::<u64>::new());
@@ -1382,7 +1597,10 @@ mod completion {
#[test] #[test]
fn deadline_survives_readback_and_encoding_then_disarms_on_completion() { fn deadline_survives_readback_and_encoding_then_disarms_on_completion() {
for result in [Ok(PathBuf::from("/tmp/shot.png")), Err("encoder failed".into())] { for result in [
Ok(PathBuf::from("/tmp/shot.png")),
Err("encoder failed".into()),
] {
let mut harness = Harness::new(2); let mut harness = Harness::new(2);
let changes = observe_deadlines(&mut harness.coordinator); let changes = observe_deadlines(&mut harness.coordinator);
let id = harness.coordinator.next_id; let id = harness.coordinator.next_id;
@@ -1392,11 +1610,19 @@ mod completion {
harness.coordinator.on_part(id, 1, Ok(buffer(2, 2))), harness.coordinator.on_part(id, 1, Ok(buffer(2, 2))),
Action::Encode { .. } Action::Encode { .. }
)); ));
assert_eq!(&*changes.borrow(), &[deadline], "encoding keeps the original deadline"); assert_eq!(
&*changes.borrow(),
&[deadline],
"encoding keeps the original deadline"
);
harness.coordinator.on_encoded(id, result); harness.coordinator.on_encoded(id, result);
assert_eq!(&*changes.borrow(), &[deadline, None]); assert_eq!(&*changes.borrow(), &[deadline, None]);
harness.coordinator.on_part(id, 0, Err("late readback".into())); harness
harness.coordinator.on_encoded(id, Err("late encode".into())); .coordinator
.on_part(id, 0, Err("late readback".into()));
harness
.coordinator
.on_encoded(id, Err("late encode".into()));
assert_eq!( assert_eq!(
changes.borrow().len(), changes.borrow().len(),
2, 2,
@@ -1440,7 +1666,11 @@ mod completion {
} }
_ => unreachable!(), _ => unreachable!(),
} }
assert_eq!(changes.borrow().last(), Some(&None), "{cancellation} must disarm"); assert_eq!(
changes.borrow().last(),
Some(&None),
"{cancellation} must disarm"
);
assert_eq!(changes.borrow().len(), 2); assert_eq!(changes.borrow().len(), 2);
assert!(harness.response().unwrap().error.is_some()); assert!(harness.response().unwrap().error.is_some());
assert!(!harness.coordinator.is_live(id)); assert!(!harness.coordinator.is_live(id));
@@ -1458,7 +1688,13 @@ mod completion {
.admit_at(1, 42, reply.clone(), vec![spec(2, 2)], now) .admit_at(1, 42, reply.clone(), vec![spec(2, 2)], now)
.unwrap(); .unwrap();
let second = coordinator let second = coordinator
.admit_at(2, 43, reply.clone(), vec![spec(2, 2)], now + Duration::from_secs(5)) .admit_at(
2,
43,
reply.clone(),
vec![spec(2, 2)],
now + Duration::from_secs(5),
)
.unwrap(); .unwrap();
coordinator.on_part(first, 0, Ok(buffer(2, 2))); coordinator.on_part(first, 0, Ok(buffer(2, 2)));
assert!( assert!(
@@ -1487,7 +1723,13 @@ mod completion {
Action::DiscardFile(_) Action::DiscardFile(_)
)); ));
coordinator coordinator
.admit_at(3, 44, reply, vec![spec(2, 2)], now + Duration::from_secs(3600)) .admit_at(
3,
44,
reply,
vec![spec(2, 2)],
now + Duration::from_secs(3600),
)
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
changes.borrow().last(), changes.borrow().last(),
@@ -1505,7 +1747,10 @@ mod completion {
assert!(!sink.publish(buffer(64, 64))); assert!(!sink.publish(buffer(64, 64)));
let PartOutcome { let PartOutcome {
request, part, result, .. request,
part,
result,
..
} = receiver.try_recv().expect("published once"); } = receiver.try_recv().expect("published once");
assert_eq!((request, part), (7, 1)); assert_eq!((request, part), (7, 1));
assert_eq!(result.unwrap().width, 2); assert_eq!(result.unwrap().width, 2);
@@ -1522,7 +1767,10 @@ mod completion {
assert!(!sink.publish(buffer(2, 2))); assert!(!sink.publish(buffer(2, 2)));
let PartOutcome { let PartOutcome {
request, part, result, .. request,
part,
result,
..
} = receiver.try_recv().expect("answered"); } = receiver.try_recv().expect("answered");
assert_eq!((request, part), (3, 0)); assert_eq!((request, part), (3, 0));
assert!(result.is_err()); assert!(result.is_err());
@@ -1913,7 +2161,11 @@ mod tests {
// 9 logical at scale 1.5. // 9 logical at scale 1.5.
assert_eq!((canvas.width, canvas.height), (14, 5)); assert_eq!((canvas.width, canvas.height), (14, 5));
for column in 0..canvas.width as usize { for column in 0..canvas.width as usize {
assert_eq!(canvas.pixels[column * 4 + 3], 255, "column {column} has a gap"); assert_eq!(
canvas.pixels[column * 4 + 3],
255,
"column {column} has a gap"
);
} }
} }
@@ -1945,7 +2197,11 @@ mod tests {
let canvas = compose(&[left, right]).unwrap(); let canvas = compose(&[left, right]).unwrap();
assert_eq!(canvas.width, 5); 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], 255);
assert_eq!(canvas.pixels[3 * 4 + 3], 255); assert_eq!(canvas.pixels[3 * 4 + 3], 255);
} }
@@ -1974,7 +2230,11 @@ mod tests {
#[test] #[test]
fn geometry_tolerates_surrounding_whitespace_from_slurp() { 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!( assert_eq!(
parse_geometry(raw).unwrap(), parse_geometry(raw).unwrap(),
Geometry::Region { Geometry::Region {
+29 -10
View File
@@ -113,8 +113,9 @@ fn encode_png(canvas: &Canvas) -> Result<Vec<u8>, String> {
if width == 0 || height == 0 { if width == 0 || height == 0 {
return Err("Screenshot is empty".into()); return Err("Screenshot is empty".into());
} }
let image = image::ImageBuffer::<image::Rgba<u8>, _>::from_raw(width, height, canvas.pixels.as_slice()) let image =
.ok_or("Screenshot buffer does not match its dimensions")?; image::ImageBuffer::<image::Rgba<u8>, _>::from_raw(width, height, canvas.pixels.as_slice())
.ok_or("Screenshot buffer does not match its dimensions")?;
let mut out = Vec::new(); let mut out = Vec::new();
let encoder = image::codecs::png::PngEncoder::new_with_quality( let encoder = image::codecs::png::PngEncoder::new_with_quality(
@@ -122,8 +123,14 @@ fn encode_png(canvas: &Canvas) -> Result<Vec<u8>, String> {
image::codecs::png::CompressionType::Fast, image::codecs::png::CompressionType::Fast,
image::codecs::png::FilterType::Adaptive, image::codecs::png::FilterType::Adaptive,
); );
image::ImageEncoder::write_image(encoder, image.as_raw(), width, height, image::ExtendedColorType::Rgba8) image::ImageEncoder::write_image(
.map_err(|error| format!("Could not encode screenshot: {error}"))?; 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"); tracing::debug!(encoded = out.len(), "encoded screenshot");
Ok(out) Ok(out)
} }
@@ -178,8 +185,8 @@ fn write_private_file_in(
} }
fn private_directory() -> Result<PathBuf, String> { fn private_directory() -> Result<PathBuf, String> {
let runtime = let runtime = std::env::var_os("XDG_RUNTIME_DIR")
std::env::var_os("XDG_RUNTIME_DIR").ok_or("XDG_RUNTIME_DIR is not set, so screenshots cannot be staged")?; .ok_or("XDG_RUNTIME_DIR is not set, so screenshots cannot be staged")?;
let root = PathBuf::from(runtime); let root = PathBuf::from(runtime);
let directory = root.join("ferese-screenshots"); let directory = root.join("ferese-screenshots");
match fs::symlink_metadata(&directory) { match fs::symlink_metadata(&directory) {
@@ -194,7 +201,9 @@ fn private_directory() -> Result<PathBuf, String> {
} }
Err(error) if error.kind() == io::ErrorKind::NotFound => { Err(error) if error.kind() == io::ErrorKind::NotFound => {
fs::create_dir(&directory) 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}"))?; .map_err(|error| format!("Could not create screenshot directory: {error}"))?;
} }
Err(error) => return Err(format!("Could not inspect screenshot directory: {error}")), Err(error) => return Err(format!("Could not inspect screenshot directory: {error}")),
@@ -248,7 +257,11 @@ mod tests {
.expect("decodes back") .expect("decodes back")
.to_rgba8(); .to_rgba8();
assert_eq!(decoded.dimensions(), (4, 3)); assert_eq!(decoded.dimensions(), (4, 3));
assert_eq!(decoded.as_raw(), &canvas.pixels, "encoding changed the pixel data"); assert_eq!(
decoded.as_raw(),
&canvas.pixels,
"encoding changed the pixel data"
);
// The opaque alpha we force is preserved through the round trip. // The opaque alpha we force is preserved through the round trip.
assert_eq!(decoded.get_pixel(0, 0)[3], 255); assert_eq!(decoded.get_pixel(0, 0)[3], 255);
} }
@@ -279,7 +292,10 @@ mod tests {
let path = write_private_file(&png).expect("staged"); let path = write_private_file(&png).expect("staged");
use std::os::unix::fs::PermissionsExt; use std::os::unix::fs::PermissionsExt;
let mode = fs::metadata(&path).unwrap().permissions().mode() & 0o777; 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(); fs::remove_file(&path).unwrap();
} }
@@ -293,7 +309,10 @@ mod tests {
fs::write(&other, b"not a screenshot").unwrap(); fs::write(&other, b"not a screenshot").unwrap();
sweep_stale_files(); 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"); assert!(other.exists(), "unrelated files are left alone");
fs::remove_file(&fresh).unwrap(); fs::remove_file(&fresh).unwrap();
+15 -10
View File
@@ -1,7 +1,9 @@
use std::sync::Arc; use std::sync::Arc;
use smithay::reexports::wayland_server::Client; use smithay::reexports::wayland_server::Client;
use smithay::wayland::security_context::{SecurityContext, SecurityContextHandler, SecurityContextListenerSource}; use smithay::wayland::security_context::{
SecurityContext, SecurityContextHandler, SecurityContextListenerSource,
};
use crate::Ferese; use crate::Ferese;
use crate::state::ClientState; use crate::state::ClientState;
@@ -14,15 +16,18 @@ pub(crate) fn unrestricted_client(client: &Client) -> bool {
impl SecurityContextHandler for Ferese { impl SecurityContextHandler for Ferese {
fn context_created(&mut self, source: SecurityContextListenerSource, context: SecurityContext) { fn context_created(&mut self, source: SecurityContextListenerSource, context: SecurityContext) {
if let Err(error) = self.loop_handle.insert_source(source, move |stream, _, state| { if let Err(error) = self
let data = ClientState { .loop_handle
security_context: Some(context.clone()), .insert_source(source, move |stream, _, state| {
..Default::default() let data = ClientState {
}; security_context: Some(context.clone()),
if let Err(error) = state.display_handle.insert_client(stream, Arc::new(data)) { ..Default::default()
tracing::warn!(%error, "failed to register sandboxed Wayland client"); };
} if let Err(error) = state.display_handle.insert_client(stream, Arc::new(data)) {
}) { tracing::warn!(%error, "failed to register sandboxed Wayland client");
}
})
{
tracing::warn!(%error, "failed to register security context listener"); tracing::warn!(%error, "failed to register security context listener");
} }
} }
+20 -5
View File
@@ -28,19 +28,28 @@ fn icon_cleanup_messages(destroy_icon: bool) -> Vec<(u32, u16, Vec<u32>)> {
.create_resource::<WlCompositor, GlobalData, Ferese>(&state.display_handle, 6, GlobalData) .create_resource::<WlCompositor, GlobalData, Ferese>(&state.display_handle, 6, GlobalData)
.unwrap(); .unwrap();
let shell = client let shell = client
.create_resource::<XdgWmBase, XdgWmBaseUserData, Ferese>(&state.display_handle, 6, Default::default()) .create_resource::<XdgWmBase, XdgWmBaseUserData, Ferese>(
&state.display_handle,
6,
Default::default(),
)
.unwrap(); .unwrap();
let shm = client let shm = client
.create_resource::<WlShm, GlobalData, Ferese>(&state.display_handle, 1, GlobalData) .create_resource::<WlShm, GlobalData, Ferese>(&state.display_handle, 1, GlobalData)
.unwrap(); .unwrap();
let manager = client let manager = client
.create_resource::<XdgToplevelIconManagerV1, GlobalData, Ferese>(&state.display_handle, 1, GlobalData) .create_resource::<XdgToplevelIconManagerV1, GlobalData, Ferese>(
&state.display_handle,
1,
GlobalData,
)
.unwrap(); .unwrap();
request(&mut wire, compositor.id().protocol_id(), 0, &[2], None); request(&mut wire, compositor.id().protocol_id(), 0, &[2], None);
request(&mut wire, shell.id().protocol_id(), 2, &[3, 2], None); request(&mut wire, shell.id().protocol_id(), 2, &[3, 2], None);
request(&mut wire, 3, 1, &[4], None); request(&mut wire, 3, 1, &[4], None);
let mut file = tempfile::tempfile().unwrap(); let mut file = tempfile::tempfile().unwrap();
file.write_all(&0xff112233u32.to_ne_bytes().repeat(32 * 32)).unwrap(); file.write_all(&0xff112233u32.to_ne_bytes().repeat(32 * 32))
.unwrap();
request( request(
&mut wire, &mut wire,
shm.id().protocol_id(), shm.id().protocol_id(),
@@ -85,9 +94,15 @@ fn destroying_an_assigned_icon_before_its_buffer_keeps_the_client_connected() {
return; return;
} }
let messages = icon_cleanup_messages(true); let messages = icon_cleanup_messages(true);
assert!(!messages.iter().any(|(object, opcode, _)| *object == 1 && *opcode == 0));
assert!( assert!(
messages.iter().any(|(object, opcode, _)| *object == 8 && *opcode == 0), !messages
.iter()
.any(|(object, opcode, _)| *object == 1 && *opcode == 0)
);
assert!(
messages
.iter()
.any(|(object, opcode, _)| *object == 8 && *opcode == 0),
"valid icon cleanup disconnected the client before its sync reply: {messages:?}" "valid icon cleanup disconnected the client before its sync reply: {messages:?}"
); );
} }
+89 -39
View File
@@ -8,7 +8,9 @@ use smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::zwlr_scre
use smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::zwlr_screencopy_frame_v1::ZwlrScreencopyFrameV1; use smithay::reexports::wayland_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_buffer::WlBuffer;
use smithay::reexports::wayland_server::protocol::wl_shm; use smithay::reexports::wayland_server::protocol::wl_shm;
use smithay::reexports::wayland_server::{Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource}; use smithay::reexports::wayland_server::{
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
};
use smithay::utils::Rectangle; use smithay::utils::Rectangle;
use smithay::wayland::shm::with_buffer_contents_mut; use smithay::wayland::shm::with_buffer_contents_mut;
@@ -34,7 +36,10 @@ struct Permit {
impl Permit { impl Permit {
fn reserve(budget: &Arc<Mutex<Budget>>, bytes: usize) -> Option<Self> { fn reserve(budget: &Arc<Mutex<Budget>>, bytes: usize) -> Option<Self> {
let mut available = budget.lock().unwrap(); 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; return None;
} }
available.bytes += bytes; available.bytes += bytes;
@@ -77,7 +82,11 @@ struct Conversion {
pub(crate) fn is_portal(stream: &std::os::unix::net::UnixStream) -> bool { pub(crate) fn is_portal(stream: &std::os::unix::net::UnixStream) -> bool {
use std::os::fd::AsRawFd; 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; 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 // SAFETY: the connected socket is live and both out pointers cover their
// initialized stack allocations. SO_PEERCRED identifies the connecting process. // initialized stack allocations. SO_PEERCRED identifies the connecting process.
@@ -95,7 +104,10 @@ pub(crate) fn is_portal(stream: &std::os::unix::net::UnixStream) -> bool {
} }
let expected = std::env::current_exe() let expected = std::env::current_exe()
.ok() .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()); .and_then(|exe| std::fs::metadata(exe).ok());
let actual = std::fs::metadata(format!("/proc/{}/exe", credentials.pid)).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()) matches!((expected, actual), (Some(expected), Some(actual)) if expected.dev() == actual.dev() && expected.ino() == actual.ino())
@@ -178,11 +190,13 @@ fn finish_conversion(state: &Ferese, result: Conversion) {
impl GlobalDispatch<FereseWindowCaptureManagerV1, Global> for Ferese { impl GlobalDispatch<FereseWindowCaptureManagerV1, Global> for Ferese {
fn can_view(client: Client, _: &Global) -> bool { fn can_view(client: Client, _: &Global) -> bool {
client.get_data::<crate::state::ClientState>().is_some_and(|state| { client
state .get_data::<crate::state::ClientState>()
.capabilities .is_some_and(|state| {
.contains(crate::private_client::ClientCapabilities::WINDOW_CAPTURE) state
}) .capabilities
.contains(crate::private_client::ClientCapabilities::WINDOW_CAPTURE)
})
} }
fn bind( fn bind(
@@ -248,7 +262,12 @@ impl Dispatch<FereseWindowCaptureManagerV1, Global> for Ferese {
}) })
.is_err() .is_err()
{ {
*resource.data::<Arc<FrameData>>().unwrap().used.lock().unwrap() = true; *resource
.data::<Arc<FrameData>>()
.unwrap()
.used
.lock()
.unwrap() = true;
manager.busy(&resource); manager.busy(&resource);
resource.failed(); resource.failed();
} }
@@ -287,7 +306,8 @@ impl Ferese {
.clone(); .clone();
let geometry = window.geometry(); let geometry = window.geometry();
let scale = output.current_scale().fractional_scale(); 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) let bytes = usize::try_from(size.w)
.ok()? .ok()?
.checked_mul(usize::try_from(size.h).ok()?)? .checked_mul(usize::try_from(size.h).ok()?)?
@@ -299,26 +319,29 @@ impl Ferese {
*busy = true; *busy = true;
return None; return None;
}; };
let cursor = if overlay_cursor && !self.overview.is_presenting() && !self.capture_cursor_protected() { let cursor =
self.seat.get_pointer().and_then(|pointer| { if overlay_cursor && !self.overview.is_presenting() && !self.capture_cursor_protected()
let location = pointer.current_location(); {
if self.window_under_visual(location).as_ref() != Some(&window) { self.seat.get_pointer().and_then(|pointer| {
return None; let location = pointer.current_location();
} if self.window_under_visual(location).as_ref() != Some(&window) {
let (x, y) = self.inverse_presented_window_point(id, location.x, location.y)?; return None;
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 (x, y) = self.inverse_presented_window_point(id, location.x, location.y)?;
if pointer.current_focus().as_ref() != Some(&surface) { let surface_point = smithay::utils::Point::from((x, y)) + geometry.loc.to_f64();
return None; let (surface, _) = window
} .surface_under(surface_point, smithay::desktop::WindowSurfaceType::ALL)?;
Some(Rectangle::new( if pointer.current_focus().as_ref() != Some(&surface) {
(location - smithay::utils::Point::from((x, y))).to_i32_round(), return None;
geometry.size, }
)) Some(Rectangle::new(
}) (location - smithay::utils::Point::from((x, y))).to_i32_round(),
} else { geometry.size,
None ))
}; })
} else {
None
};
let result = if let Some(backend) = self.nested_backend.clone() { let result = if let Some(backend) = self.nested_backend.clone() {
let mut backend = backend.try_borrow_mut().ok()?; let mut backend = backend.try_borrow_mut().ok()?;
crate::render::capture_window_frame( crate::render::capture_window_frame(
@@ -357,10 +380,18 @@ impl Ferese {
} }
} }
pub(super) fn copy_snapshot(state: &Ferese, resource: &ZwlrScreencopyFrameV1, data: &FrameData, buffer: &WlBuffer) { pub(super) fn copy_snapshot(
state: &Ferese,
resource: &ZwlrScreencopyFrameV1,
data: &FrameData,
buffer: &WlBuffer,
) {
let mut used = data.used.lock().unwrap(); let mut used = data.used.lock().unwrap();
if *used { 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; return;
} }
*used = true; *used = true;
@@ -393,7 +424,11 @@ pub(super) fn copy_snapshot(state: &Ferese, resource: &ZwlrScreencopyFrameV1, da
// SAFETY: Smithay owns the writable mapping, validated format/stride and // SAFETY: Smithay owns the writable mapping, validated format/stride and
// checked offset cover the complete snapshot; the source is separately owned. // checked offset cover the complete snapshot; the source is separately owned.
unsafe { 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 true
}) })
@@ -459,7 +494,11 @@ pub(crate) mod tests {
.unwrap(); .unwrap();
let data = frame_data(); let data = frame_data();
let frame = client let frame = client
.create_resource::<ZwlrScreencopyFrameV1, Arc<FrameData>, Ferese>(&state.display_handle, 3, data.clone()) .create_resource::<ZwlrScreencopyFrameV1, Arc<FrameData>, Ferese>(
&state.display_handle,
3,
data.clone(),
)
.unwrap(); .unwrap();
finish_conversion( finish_conversion(
state, state,
@@ -475,20 +514,27 @@ pub(crate) mod tests {
let data = frame_data(); let data = frame_data();
*data.snapshot.lock().unwrap() = Some(snapshot()); *data.snapshot.lock().unwrap() = Some(snapshot());
let frame = client let frame = client
.create_resource::<ZwlrScreencopyFrameV1, Arc<FrameData>, Ferese>(&state.display_handle, 3, data.clone()) .create_resource::<ZwlrScreencopyFrameV1, Arc<FrameData>, Ferese>(
&state.display_handle,
3,
data.clone(),
)
.unwrap(); .unwrap();
let buffer = client let buffer = client
.object_from_protocol_id::<WlBuffer>(&state.display_handle, 6) .object_from_protocol_id::<WlBuffer>(&state.display_handle, 6)
.unwrap(); .unwrap();
let before = with_buffer_contents_mut(&buffer, |ptr, _, layout| { let before = with_buffer_contents_mut(&buffer, |ptr, _, layout| {
// SAFETY: the fixture allocates and validates this 64x48 SHM buffer. // SAFETY: the fixture allocates and validates this 64x48 SHM buffer.
unsafe { std::slice::from_raw_parts(ptr.add(layout.offset as usize), 64 * 48 * 4).to_vec() } unsafe {
std::slice::from_raw_parts(ptr.add(layout.offset as usize), 64 * 48 * 4).to_vec()
}
}) })
.unwrap(); .unwrap();
copy_snapshot(state, &frame, &data, &buffer); copy_snapshot(state, &frame, &data, &buffer);
with_buffer_contents_mut(&buffer, |ptr, _, layout| { with_buffer_contents_mut(&buffer, |ptr, _, layout| {
// SAFETY: same fixture-owned buffer as above. // SAFETY: same fixture-owned buffer as above.
let after = unsafe { std::slice::from_raw_parts(ptr.add(layout.offset as usize), 64 * 48 * 4) }; let after =
unsafe { std::slice::from_raw_parts(ptr.add(layout.offset as usize), 64 * 48 * 4) };
if allowed { if allowed {
assert!(after.iter().all(|byte| *byte == 0)); assert!(after.iter().all(|byte| *byte == 0));
} else { } else {
@@ -496,7 +542,11 @@ pub(crate) mod tests {
} }
// Restore the client's colored buffer for subsequent renderer checks. // Restore the client's colored buffer for subsequent renderer checks.
unsafe { unsafe {
std::ptr::copy_nonoverlapping(before.as_ptr(), ptr.add(layout.offset as usize), before.len()); std::ptr::copy_nonoverlapping(
before.as_ptr(),
ptr.add(layout.offset as usize),
before.len(),
);
} }
}) })
.unwrap(); .unwrap();
+51 -18
View File
@@ -60,7 +60,9 @@ impl XdgShellHandler for Ferese {
fn parent_changed(&mut self, surface: ToplevelSurface) { fn parent_changed(&mut self, surface: ToplevelSurface) {
self.refresh_capture_privacy(); self.refresh_capture_privacy();
self.refresh_idle_inhibition(); self.refresh_idle_inhibition();
let parent = surface.parent().and_then(|parent| self.windows.id_for_surface(&parent)); let parent = surface
.parent()
.and_then(|parent| self.windows.id_for_surface(&parent));
let child = self let child = self
.windows .windows
.id_for_surface(surface.wl_surface()) .id_for_surface(surface.wl_surface())
@@ -95,7 +97,12 @@ impl XdgShellHandler for Ferese {
} }
} }
fn reposition_request(&mut self, surface: PopupSurface, positioner: PositionerState, token: u32) { fn reposition_request(
&mut self,
surface: PopupSurface,
positioner: PositionerState,
token: u32,
) {
surface.with_pending_state(|state| { surface.with_pending_state(|state| {
state.geometry = positioner.get_geometry(); state.geometry = positioner.get_geometry();
state.positioner = positioner; state.positioner = positioner;
@@ -252,7 +259,12 @@ impl XdgShellHandler for Ferese {
}; };
if let Some(pointer) = seat.get_pointer() { 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() { if let Some(keyboard) = seat.get_keyboard() {
keyboard.set_grab(self, PopupKeyboardGrab::new(&popup_grab), serial); keyboard.set_grab(self, PopupKeyboardGrab::new(&popup_grab), serial);
@@ -264,14 +276,21 @@ impl XdgShellHandler for Ferese {
} }
} }
fn check_grab(seat: &Seat<Ferese>, surface: &WlSurface, serial: Serial) -> Option<GrabStartData<Ferese>> { fn check_grab(
seat: &Seat<Ferese>,
surface: &WlSurface,
serial: Serial,
) -> Option<GrabStartData<Ferese>> {
let pointer = seat.get_pointer()?; let pointer = seat.get_pointer()?;
if !pointer.has_grab(serial) { if !pointer.has_grab(serial) {
return None; return None;
} }
let start_data = pointer.grab_start_data()?; let start_data = pointer.grab_start_data()?;
let (focus, _) = start_data.focus.as_ref()?; 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 { impl XdgDecorationHandler for Ferese {
@@ -325,12 +344,14 @@ pub(crate) fn validate_size_constraints(toplevel: &ToplevelSurface) -> bool {
let mut cached = states.cached_state.get::<SurfaceCachedState>(); let mut cached = states.cached_state.get::<SurfaceCachedState>();
let state = cached.current(); let state = cached.current();
let valid_axis = |min: i32, max: i32| min >= 0 && max >= 0 && (max == 0 || min <= max); let valid_axis = |min: i32, max: i32| min >= 0 && max >= 0 && (max == 0 || min <= max);
valid_axis(state.min_size.w, state.max_size.w) && valid_axis(state.min_size.h, state.max_size.h) valid_axis(state.min_size.w, state.max_size.w)
&& valid_axis(state.min_size.h, state.max_size.h)
}); });
if !valid { if !valid {
toplevel toplevel.xdg_toplevel().post_error(
.xdg_toplevel() xdg_toplevel::Error::InvalidSize,
.post_error(xdg_toplevel::Error::InvalidSize, "invalid minimum/maximum window size"); "invalid minimum/maximum window size",
);
} }
valid valid
@@ -359,7 +380,9 @@ pub fn handle_commit(state: &mut Ferese, surface: &WlSurface) {
if let Some(PopupKind::Xdg(popup)) = state.popups.find_popup(surface) if let Some(PopupKind::Xdg(popup)) = state.popups.find_popup(surface)
&& !popup.is_initial_configure_sent() && !popup.is_initial_configure_sent()
{ {
popup.send_configure().expect("initial popup configure is valid"); popup
.send_configure()
.expect("initial popup configure is valid");
} }
} }
@@ -415,10 +438,11 @@ impl Ferese {
let Ok(root) = find_popup_root_surface(&PopupKind::Xdg(popup.clone())) else { let Ok(root) = find_popup_root_surface(&PopupKind::Xdg(popup.clone())) else {
return; return;
}; };
let window = self let window = self.space.elements().find(|window| {
.space window
.elements() .toplevel()
.find(|window| window.toplevel().is_some_and(|toplevel| toplevel.wl_surface() == &root)); .is_some_and(|toplevel| toplevel.wl_surface() == &root)
});
let geometry = (|| { let geometry = (|| {
if let Some(window) = window { if let Some(window) = window {
let id = *self.windows.ids().get(window)?; let id = *self.windows.ids().get(window)?;
@@ -432,11 +456,14 @@ impl Ferese {
Some((output_geometry, location)) Some((output_geometry, location))
} else { } else {
let layer = self.space.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL)?; let layer = self
.space
.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL)?;
self.space.outputs().find_map(|output| { self.space.outputs().find_map(|output| {
let output_geometry = self.space.output_geometry(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)) Some((output_geometry, output_geometry.loc + layer_geometry.loc))
}) })
@@ -490,7 +517,9 @@ impl Dispatch<xdg_toplevel::XdgToplevel, XdgShellSurfaceUserData> for Ferese {
return; return;
} }
smithay::wayland::Dispatch2::request(data, state, client, resource, request, dhandle, data_init); smithay::wayland::Dispatch2::request(
data, state, client, resource, request, dhandle, data_init,
);
} }
fn destroyed( fn destroyed(
@@ -524,7 +553,11 @@ fn size_limits_usable(
(pending.max_size.w, pending.max_size.h), (pending.max_size.w, pending.max_size.h),
) )
}); });
let (min, max) = if minimum { (proposed, max) } else { (min, proposed) }; let (min, max) = if minimum {
(proposed, max)
} else {
(min, proposed)
};
(max.0 == 0 || min.0 <= max.0) && (max.1 == 0 || min.1 <= max.1) (max.0 == 0 || min.0 <= max.0) && (max.1 == 0 || min.1 <= max.1)
} }
+30 -10
View File
@@ -77,16 +77,21 @@ fn player_window(player: &media::Player, windows: &[AppWindow<'_>]) -> Option<Wi
impl Ferese { impl Ferese {
pub(crate) fn refresh_idle_inhibition(&mut self) { pub(crate) fn refresh_idle_inhibition(&mut self) {
let playing = self.media_players.iter().any(|player| player.playing); let playing = self.media_players.iter().any(|player| player.playing);
let has_rules = self.window_rules.iter().any(|rule| rule.idle_policy().is_some()); let has_rules = self
.window_rules
.iter()
.any(|rule| rule.idle_policy().is_some());
let selected = self let selected = self
.window_rules .window_rules
.iter() .iter()
.any(|rule| rule.idle_policy().is_some_and(|mode| mode != Mode::None)); .any(|rule| rule.idle_policy().is_some_and(|mode| mode != Mode::None));
let playback_policy = self.idle_inhibit.fullscreen_playback let playback_policy = self.idle_inhibit.fullscreen_playback
|| self || self.window_rules.iter().any(|rule| {
.window_rules matches!(
.iter() rule.idle_policy(),
.any(|rule| matches!(rule.idle_policy(), Some(Mode::Playing | Mode::FullscreenPlaying))); Some(Mode::Playing | Mode::FullscreenPlaying)
)
});
let automatic = (playback_policy && playing) || selected; let automatic = (playback_policy && playing) || selected;
let eligible = if self.idle_inhibitors.is_empty() && !automatic { let eligible = if self.idle_inhibitors.is_empty() && !automatic {
Default::default() Default::default()
@@ -195,9 +200,15 @@ mod tests {
"idle-inhibit { fullscreen-playback #false; }\nwindow-rule app-id=\"player\" idle-inhibit=\"visible\"\nwindow-rule app-id=\"player\" title=\"Private\" idle-inhibit=\"none\"\n", "idle-inhibit { fullscreen-playback #false; }\nwindow-rule app-id=\"player\" idle-inhibit=\"visible\"\nwindow-rule app-id=\"player\" title=\"Private\" idle-inhibit=\"none\"\n",
).unwrap().runtime_config().unwrap(); ).unwrap().runtime_config().unwrap();
assert!(!config.idle_inhibit.fullscreen_playback); assert!(!config.idle_inhibit.fullscreen_playback);
let visible = crate::window_rules::resolve(&config.window_rules, Some("player"), None, false); let visible =
crate::window_rules::resolve(&config.window_rules, Some("player"), None, false);
assert_eq!(visible.idle_inhibit, Some(Mode::Visible)); assert_eq!(visible.idle_inhibit, Some(Mode::Visible));
let none = crate::window_rules::resolve(&config.window_rules, Some("player"), Some("Private"), false); let none = crate::window_rules::resolve(
&config.window_rules,
Some("player"),
Some("Private"),
false,
);
assert_eq!(none.idle_inhibit, Some(Mode::None)); assert_eq!(none.idle_inhibit, Some(Mode::None));
assert_eq!(crate::window_rules::live_result(visible, none, false), None); assert_eq!(crate::window_rules::live_result(visible, none, false), None);
for mode in ["playing", "fullscreen", "fullscreen-playing", "none"] { for mode in ["playing", "fullscreen", "fullscreen-playing", "none"] {
@@ -211,9 +222,15 @@ mod tests {
); );
} }
assert!( assert!(
crate::config::Config::parse_source("window-rule app-id=\"player\" idle-inhibit=\"alwayss\"\n").is_err() crate::config::Config::parse_source(
"window-rule app-id=\"player\" idle-inhibit=\"alwayss\"\n"
)
.is_err()
);
assert!(
crate::config::Config::parse_source("idle-inhibit { fullscreen-playback \"yes\"; }\n")
.is_err()
); );
assert!(crate::config::Config::parse_source("idle-inhibit { fullscreen-playback \"yes\"; }\n").is_err());
} }
#[test] #[test]
@@ -236,7 +253,10 @@ mod tests {
pid: Some(99), pid: Some(99),
app_id: "Player.desktop", app_id: "Player.desktop",
}; };
assert_eq!(player_window(&player, &[window, unrelated]), Some(window.id)); assert_eq!(
player_window(&player, &[window, unrelated]),
Some(window.id)
);
assert_eq!(player_window(&player, &[unrelated]), Some(unrelated.id)); assert_eq!(player_window(&player, &[unrelated]), Some(unrelated.id));
assert_eq!(player_window(&player, &[]), None); assert_eq!(player_window(&player, &[]), None);
assert_eq!( assert_eq!(
+29 -13
View File
@@ -57,7 +57,8 @@ impl Engine {
self.locked = locked; self.locked = locked;
if locked { if locked {
for (_, (id, response)) in self.waiters.drain() { for (_, (id, response)) in self.waiters.drain() {
let _ = response.try_send(Response::error(id, "session_locked", "Session is locked")); let _ =
response.try_send(Response::error(id, "session_locked", "Session is locked"));
} }
} }
} }
@@ -84,7 +85,13 @@ impl Engine {
} }
} }
pub(crate) fn watch(&mut self, owner: u64, id: u64, since: u64, response: mpsc::SyncSender<Response>) { pub(crate) fn watch(
&mut self,
owner: u64,
id: u64,
since: u64,
response: mpsc::SyncSender<Response>,
) {
if self.locked { if self.locked {
let _ = response.try_send(Response::error(id, "session_locked", "Session is locked")); let _ = response.try_send(Response::error(id, "session_locked", "Session is locked"));
} else if since != self.snapshot.revision { } else if since != self.snapshot.revision {
@@ -98,8 +105,15 @@ impl Engine {
self.waiters.remove(&owner); self.waiters.remove(&owner);
} }
pub(crate) fn action(&self, args: Value, reply: Option<(u64, mpsc::SyncSender<Response>)>) -> Result<(), String> { pub(crate) fn action(
let commands = self.commands.as_ref().ok_or("Media service is unavailable")?; &self,
args: Value,
reply: Option<(u64, mpsc::SyncSender<Response>)>,
) -> Result<(), String> {
let commands = self
.commands
.as_ref()
.ok_or("Media service is unavailable")?;
commands commands
.try_send(Command { .try_send(Command {
args, args,
@@ -113,16 +127,18 @@ impl Engine {
pub(crate) fn init(event_loop: &mut EventLoop<Ferese>) -> Result<(), Box<dyn std::error::Error>> { pub(crate) fn init(event_loop: &mut EventLoop<Ferese>) -> Result<(), Box<dyn std::error::Error>> {
let (sender, receiver) = sync_channel::<Update>(1); let (sender, receiver) = sync_channel::<Update>(1);
let (commands, receive_commands) = async_channel::bounded(16); let (commands, receive_commands) = async_channel::bounded(16);
event_loop.handle().insert_source(receiver, move |event, _, state| { event_loop
if let Event::Msg(update) = event { .handle()
let changed = inhibition_changed(&state.media_players, &update.players); .insert_source(receiver, move |event, _, state| {
state.media_players = update.players; if let Event::Msg(update) = event {
state.media_engine.update(update.snapshot); let changed = inhibition_changed(&state.media_players, &update.players);
if changed { state.media_players = update.players;
state.refresh_idle_inhibition(); state.media_engine.update(update.snapshot);
if changed {
state.refresh_idle_inhibition();
}
} }
} })?;
})?;
event_loop event_loop
.handle() .handle()
.insert_idle(move |state| state.media_engine.commands = Some(commands)); .insert_idle(move |state| state.media_engine.commands = Some(commands));
+63 -19
View File
@@ -38,7 +38,11 @@ async fn bus_call<T: serde::de::DeserializeOwned + zbus::zvariant::Type>(
.deserialize() .deserialize()
} }
async fn properties(connection: &Connection, owner: &str, interface: &str) -> zbus::Result<Properties> { async fn properties(
connection: &Connection,
owner: &str,
interface: &str,
) -> zbus::Result<Properties> {
connection connection
.call_method(Some(owner), PATH, Some(PROPERTIES), "GetAll", &(interface,)) .call_method(Some(owner), PATH, Some(PROPERTIES), "GetAll", &(interface,))
.await? .await?
@@ -47,7 +51,9 @@ async fn properties(connection: &Connection, owner: &str, interface: &str) -> zb
} }
fn string<'a>(properties: &'a Properties, name: &str) -> Option<&'a str> { fn string<'a>(properties: &'a Properties, name: &str) -> Option<&'a str> {
properties.get(name).and_then(|value| <&str>::try_from(value).ok()) properties
.get(name)
.and_then(|value| <&str>::try_from(value).ok())
} }
pub(super) fn clean(value: &str, length: usize) -> String { pub(super) fn clean(value: &str, length: usize) -> String {
@@ -163,7 +169,11 @@ async fn discover(connection: &Connection, name: String, owner: String) -> Playe
player player
} }
async fn action(connection: &Connection, state: &mut PlayersState, args: &serde_json::Value) -> Result<(), String> { async fn action(
connection: &Connection,
state: &mut PlayersState,
args: &serde_json::Value,
) -> Result<(), String> {
let kind = args["action"].as_str().ok_or("Missing media action")?; let kind = args["action"].as_str().ok_or("Missing media action")?;
if kind == "auto" { if kind == "auto" {
state.pinned = None; state.pinned = None;
@@ -176,7 +186,10 @@ async fn action(connection: &Connection, state: &mut PlayersState, args: &serde_
} }
.cloned() .cloned()
.ok_or("No active media player")?; .ok_or("No active media player")?;
if args["owner"].as_str().is_some_and(|owner| owner != player.owner) { if args["owner"]
.as_str()
.is_some_and(|owner| owner != player.owner)
{
return Err("Media player was replaced".into()); return Err("Media player was replaced".into());
} }
@@ -227,12 +240,15 @@ async fn action(connection: &Connection, state: &mut PlayersState, args: &serde_
if args["track_id"].as_str() != view.track_id.as_deref() { if args["track_id"].as_str() != view.track_id.as_deref() {
return Err("Media track changed".into()); return Err("Media track changed".into());
} }
let position = args["position_us"].as_u64().ok_or("Invalid media position")?; let position = args["position_us"]
.as_u64()
.ok_or("Invalid media position")?;
let position = view let position = view
.length_us .length_us
.map_or(position, |length| position.min(length)) .map_or(position, |length| position.min(length))
.min(i64::MAX as u64) as i64; .min(i64::MAX as u64) as i64;
let track = OwnedObjectPath::try_from(view.track_id.clone().unwrap()).map_err(|error| error.to_string())?; let track = OwnedObjectPath::try_from(view.track_id.clone().unwrap())
.map_err(|error| error.to_string())?;
connection connection
.call_method( .call_method(
Some(player.owner.as_str()), Some(player.owner.as_str()),
@@ -279,7 +295,11 @@ async fn action(connection: &Connection, state: &mut PlayersState, args: &serde_
Ok(()) Ok(())
} }
fn publish(state: &PlayersState, previous: &mut Option<Update>, sender: &SyncSender<Update>) -> bool { fn publish(
state: &PlayersState,
previous: &mut Option<Update>,
sender: &SyncSender<Update>,
) -> bool {
let update = state.update_snapshot(); let update = state.update_snapshot();
if previous if previous
.as_ref() .as_ref()
@@ -336,7 +356,10 @@ async fn monitor(
.deserialize()?; .deserialize()?;
let mut names: Vec<_> = names let mut names: Vec<_> = names
.into_iter() .into_iter()
.filter(|name| name.strip_prefix(PREFIX).is_some_and(|suffix| suffix.starts_with('.'))) .filter(|name| {
name.strip_prefix(PREFIX)
.is_some_and(|suffix| suffix.starts_with('.'))
})
.collect(); .collect();
names.sort_unstable(); names.sort_unstable();
for name in names.into_iter().take(MAX_PLAYERS) { for name in names.into_iter().take(MAX_PLAYERS) {
@@ -367,7 +390,9 @@ async fn monitor(
}; };
match input { match input {
Input::Action(None) => return Ok(()), Input::Action(None) => return Ok(()),
Input::Signal(None) => return Err(zbus::Error::Failure("MPRIS signal stream ended".into())), Input::Signal(None) => {
return Err(zbus::Error::Failure("MPRIS signal stream ended".into()));
}
Input::Action(Some(command)) => { Input::Action(Some(command)) => {
let result = if command.sent.elapsed() > Duration::from_secs(2) { let result = if command.sent.elapsed() > Duration::from_secs(2) {
Err("Media command expired".into()) Err("Media command expired".into())
@@ -389,8 +414,12 @@ async fn monitor(
.member() .member()
.is_some_and(|member| member.as_str() == "NameOwnerChanged") .is_some_and(|member| member.as_str() == "NameOwnerChanged")
{ {
let (name, _, owner): (String, String, String) = message.body().deserialize()?; let (name, _, owner): (String, String, String) =
if !name.strip_prefix(PREFIX).is_some_and(|suffix| suffix.starts_with('.')) { message.body().deserialize()?;
if !name
.strip_prefix(PREFIX)
.is_some_and(|suffix| suffix.starts_with('.'))
{
continue; continue;
} }
state.remove(&name); state.remove(&name);
@@ -402,7 +431,10 @@ async fn monitor(
state.update(player); state.update(player);
} }
} else if let Some(owner) = header.sender() { } else if let Some(owner) = header.sender() {
if header.member().is_some_and(|member| member.as_str() == "Seeked") { if header
.member()
.is_some_and(|member| member.as_str() == "Seeked")
{
let names: Vec<_> = state let names: Vec<_> = state
.players .players
.values() .values()
@@ -417,14 +449,17 @@ async fn monitor(
} }
} else { } else {
let Ok((interface, changed, invalidated)) = let Ok((interface, changed, invalidated)) =
message.body().deserialize::<(String, Properties, Vec<String>)>() message
.body()
.deserialize::<(String, Properties, Vec<String>)>()
else { else {
continue; continue;
}; };
let identity = interface == PREFIX let identity = interface == PREFIX
&& ["Identity", "DesktopEntry", "CanRaise"] && ["Identity", "DesktopEntry", "CanRaise"].iter().any(|key| {
.iter() changed.contains_key(*key)
.any(|key| changed.contains_key(*key) || invalidated.iter().any(|field| field == key)); || invalidated.iter().any(|field| field == key)
});
let playback = interface == PLAYER let playback = interface == PLAYER
&& [ && [
"PlaybackStatus", "PlaybackStatus",
@@ -439,7 +474,10 @@ async fn monitor(
"CanControl", "CanControl",
] ]
.iter() .iter()
.any(|key| changed.contains_key(*key) || invalidated.iter().any(|field| field == key)); .any(|key| {
changed.contains_key(*key)
|| invalidated.iter().any(|field| field == key)
});
if !identity && !playback { if !identity && !playback {
continue; continue;
} }
@@ -483,10 +521,16 @@ pub(super) fn run(sender: SyncSender<Update>, commands: async_channel::Receiver<
if matches!(result, Ok(())) || commands.is_closed() { if matches!(result, Ok(())) || commands.is_closed() {
break; break;
} }
if sender.send(PlayersState::default().update_snapshot()).is_err() { if sender
.send(PlayersState::default().update_snapshot())
.is_err()
{
break; break;
} }
tracing::debug!(?result, "MPRIS disconnected; clearing playback and retrying"); tracing::debug!(
?result,
"MPRIS disconnected; clearing playback and retrying"
);
std::thread::sleep(Duration::from_secs(5)); std::thread::sleep(Duration::from_secs(5));
} }
} }
@@ -47,8 +47,9 @@ impl PlayersState {
} }
pub fn selected(&self) -> Option<&Player> { pub fn selected(&self) -> Option<&Player> {
let eligible = let eligible = |player: &&Player| {
|player: &&Player| player.view.status != Playback::Stopped && !self.ignored.contains(&player.name); player.view.status != Playback::Stopped && !self.ignored.contains(&player.name)
};
if let Some(player) = self if let Some(player) = self
.pinned .pinned
.as_ref() .as_ref()
@@ -55,7 +55,9 @@ fn pin_ignore_and_departure_do_not_leave_a_ghost_player() {
fn initial_ties_are_deterministic_and_stopped_players_are_hidden() { fn initial_ties_are_deterministic_and_stopped_players_are_hidden() {
let mut state = PlayersState::default(); let mut state = PlayersState::default();
for name in ["z", "a"] { for name in ["z", "a"] {
state.players.insert(name.into(), player(name, Playback::Playing)); state
.players
.insert(name.into(), player(name, Playback::Playing));
} }
assert_eq!(state.selected().unwrap().name, "a"); assert_eq!(state.selected().unwrap().name, "a");
state.update(player("a", Playback::Stopped)); state.update(player("a", Playback::Stopped));
@@ -132,7 +134,10 @@ fn session_lock_cancels_pending_and_rejects_new_media_watches() {
for since in [0, engine.snapshot.revision] { for since in [0, engine.snapshot.revision] {
let (send, receive) = mpsc::sync_channel(1); let (send, receive) = mpsc::sync_channel(1);
engine.watch(2, 43, since, send); engine.watch(2, 43, since, send);
assert_eq!(receive.recv().unwrap().error.unwrap().code, "session_locked"); assert_eq!(
receive.recv().unwrap().error.unwrap().code,
"session_locked"
);
} }
engine.set_locked(false); engine.set_locked(false);
let (send, receive) = mpsc::sync_channel(1); let (send, receive) = mpsc::sync_channel(1);
+149 -63
View File
@@ -1,13 +1,14 @@
use ferese_layout::Direction; use ferese_layout::Direction;
use smithay::backend::input::{ use smithay::backend::input::{
AbsolutePositionEvent, Axis, AxisSource, ButtonState, Event, GestureBeginEvent, GestureEndEvent, AbsolutePositionEvent, Axis, AxisSource, ButtonState, Event, GestureBeginEvent,
GestureSwipeUpdateEvent as _, InputBackend, InputEvent, KeyState, KeyboardKeyEvent, PointerAxisEvent, GestureEndEvent, GestureSwipeUpdateEvent as _, InputBackend, InputEvent, KeyState,
PointerButtonEvent, PointerMotionEvent, Switch, SwitchState, SwitchToggleEvent, TouchEvent, KeyboardKeyEvent, PointerAxisEvent, PointerButtonEvent, PointerMotionEvent, Switch,
SwitchState, SwitchToggleEvent, TouchEvent,
}; };
use smithay::input::keyboard::{FilterResult, keysyms}; use smithay::input::keyboard::{FilterResult, keysyms};
use smithay::input::pointer::{ use smithay::input::pointer::{
AxisFrame, ButtonEvent, Focus, GestureSwipeBeginEvent, GestureSwipeEndEvent, GestureSwipeUpdateEvent, AxisFrame, ButtonEvent, Focus, GestureSwipeBeginEvent, GestureSwipeEndEvent,
GrabStartData, MotionEvent, PointerHandle, RelativeMotionEvent, GestureSwipeUpdateEvent, GrabStartData, MotionEvent, PointerHandle, RelativeMotionEvent,
}; };
use smithay::input::touch::{DownEvent, MotionEvent as TouchMotionEvent, UpEvent}; use smithay::input::touch::{DownEvent, MotionEvent as TouchMotionEvent, UpEvent};
use smithay::reexports::wayland_server::Resource; use smithay::reexports::wayland_server::Resource;
@@ -63,7 +64,8 @@ impl Ferese {
let layers = smithay::desktop::layer_map_for_output(output); let layers = smithay::desktop::layer_map_for_output(output);
for layer in layers.layers() { for layer in layers.layers() {
let Some(local) = surface_tree_position(layer.wl_surface(), surface, (0, 0).into()) else { let Some(local) = surface_tree_position(layer.wl_surface(), surface, (0, 0).into())
else {
continue; continue;
}; };
let geometry = layers.layer_geometry(layer)?; let geometry = layers.layer_geometry(layer)?;
@@ -135,7 +137,9 @@ impl Ferese {
if !matches!( if !matches!(
&event, &event,
InputEvent::DeviceAdded { .. } | InputEvent::DeviceRemoved { .. } | InputEvent::SwitchToggle { .. } InputEvent::DeviceAdded { .. }
| InputEvent::DeviceRemoved { .. }
| InputEvent::SwitchToggle { .. }
) { ) {
self.lock_input_activity(); self.lock_input_activity();
} }
@@ -221,32 +225,39 @@ impl Ferese {
} }
BindingAction::Focus(focus @ (Direction::Left | Direction::Right)) => { BindingAction::Focus(focus @ (Direction::Left | Direction::Right)) => {
self.swipe.mark_momentum_navigation(); self.swipe.mark_momentum_navigation();
let raw = self.swipe.unbounded_preview().map_or(progress, |(_, value)| value); let raw = self
.swipe
.unbounded_preview()
.map_or(progress, |(_, value)| value);
self.preview_focus_swipe(focus, direction, raw); self.preview_focus_swipe(focus, direction, raw);
} }
_ => {} _ => {}
} }
} }
} else { } else {
seat.get_pointer().expect("seat has a pointer").gesture_swipe_update( seat.get_pointer()
self, .expect("seat has a pointer")
&GestureSwipeUpdateEvent { .gesture_swipe_update(
time: event.time(), self,
delta: event.delta(), &GestureSwipeUpdateEvent {
}, time: event.time(),
); delta: event.delta(),
},
);
} }
} }
InputEvent::GestureSwipeEnd { event } => { InputEvent::GestureSwipeEnd { event } => {
if self.swipe.active() { if self.swipe.active() {
let fingers = self.swipe.fingers(); let fingers = self.swipe.fingers();
let cancelled = event.cancelled() || self.swipe_navigation_blocked(); let cancelled = event.cancelled() || self.swipe_navigation_blocked();
let direction = self let direction = self.swipe.finish_at(
.swipe cancelled,
.finish_at(cancelled, std::time::Duration::from_micros(event.time().micros())); std::time::Duration::from_micros(event.time().micros()),
);
let workspace_handled = self.finish_workspace_swipe(direction); let workspace_handled = self.finish_workspace_swipe(direction);
let focus_handled = self.finish_focus_swipe(direction); let focus_handled = self.finish_focus_swipe(direction);
let handled = workspace_handled || focus_handled || self.swipe.preview_started(); let handled =
workspace_handled || focus_handled || self.swipe.preview_started();
if !handled if !handled
&& let Some(direction) = direction && let Some(direction) = direction
&& let Some(action) = self && let Some(action) = self
@@ -256,7 +267,9 @@ impl Ferese {
.map(|binding| binding.action.clone()) .map(|binding| binding.action.clone())
{ {
match action { match action {
BindingAction::Focus(direction @ (Direction::Left | Direction::Right)) => { BindingAction::Focus(
direction @ (Direction::Left | Direction::Right),
) => {
self.focus_direction_from_swipe(direction); self.focus_direction_from_swipe(direction);
} }
BindingAction::SwitchRelativeWorkspace(next) => { BindingAction::SwitchRelativeWorkspace(next) => {
@@ -266,14 +279,16 @@ impl Ferese {
} }
} }
} else { } else {
seat.get_pointer().expect("seat has a pointer").gesture_swipe_end( seat.get_pointer()
self, .expect("seat has a pointer")
&GestureSwipeEndEvent { .gesture_swipe_end(
serial: SERIAL_COUNTER.next_serial(), self,
time: event.time(), &GestureSwipeEndEvent {
cancelled: event.cancelled(), serial: SERIAL_COUNTER.next_serial(),
}, time: event.time(),
); cancelled: event.cancelled(),
},
);
} }
} }
InputEvent::SwitchToggle { event } if event.switch() == Some(Switch::Lid) => { InputEvent::SwitchToggle { event } if event.switch() == Some(Switch::Lid) => {
@@ -544,7 +559,8 @@ impl Ferese {
focus, focus,
&RelativeMotionEvent { &RelativeMotionEvent {
delta: (delta.x / scale_x, delta.y / scale_y).into(), 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(),
time: event.time(), time: event.time(),
}, },
); );
@@ -574,7 +590,8 @@ impl Ferese {
} }
return; return;
} }
if event.state() == ButtonState::Released && self.consumed_pointer_buttons.remove(&event.button_code()) if event.state() == ButtonState::Released
&& self.consumed_pointer_buttons.remove(&event.button_code())
{ {
return; return;
} }
@@ -733,12 +750,12 @@ impl Ferese {
InputEvent::PointerAxis { event, .. } => { InputEvent::PointerAxis { event, .. } => {
self.last_pointer_time = event.time(); self.last_pointer_time = event.time();
let source = event.source(); let source = event.source();
let horizontal = event let horizontal = event.amount(Axis::Horizontal).unwrap_or_else(|| {
.amount(Axis::Horizontal) event.amount_v120(Axis::Horizontal).unwrap_or(0.0) * 15.0 / 120.0
.unwrap_or_else(|| event.amount_v120(Axis::Horizontal).unwrap_or(0.0) * 15.0 / 120.0); });
let vertical = event let vertical = event.amount(Axis::Vertical).unwrap_or_else(|| {
.amount(Axis::Vertical) event.amount_v120(Axis::Vertical).unwrap_or(0.0) * 15.0 / 120.0
.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.active() {
if self.input_capture.captures(2) { 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)})); 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)}));
@@ -748,13 +765,21 @@ impl Ferese {
let pointer = self.seat.get_pointer().expect("seat has a pointer"); let pointer = self.seat.get_pointer().expect("seat has a pointer");
if self.scroll_tab_indicator( if self.scroll_tab_indicator(
pointer.current_location(), pointer.current_location(),
if vertical != 0.0 { vertical } else { horizontal }, if vertical != 0.0 {
vertical
} else {
horizontal
},
) { ) {
return; return;
} }
if self.scroll_overview_strip( if self.scroll_overview_strip(
pointer.current_location(), pointer.current_location(),
if horizontal != 0.0 { horizontal } else { vertical }, if horizontal != 0.0 {
horizontal
} else {
vertical
},
) { ) {
return; return;
} }
@@ -791,7 +816,8 @@ impl Ferese {
let Some(location) = self.absolute_event_position(&event) else { let Some(location) = self.absolute_event_position(&event) else {
return; return;
}; };
if !self.consumed_touch_slots.is_empty() || self.dismiss_launcher_outside(location) { if !self.consumed_touch_slots.is_empty() || self.dismiss_launcher_outside(location)
{
self.consumed_touch_slots.insert(event.slot()); self.consumed_touch_slots.insert(event.slot());
return; return;
} }
@@ -800,7 +826,10 @@ impl Ferese {
let serial = SERIAL_COUNTER.next_serial(); let serial = SERIAL_COUNTER.next_serial();
if !touch.is_grabbed() { if !touch.is_grabbed() {
self.record_activation_input(serial, self.surface_under(location).map(|(surface, _)| surface)); self.record_activation_input(
serial,
self.surface_under(location).map(|(surface, _)| surface),
);
} }
touch.down( touch.down(
@@ -865,7 +894,11 @@ impl Ferese {
if self.session_lock.active() || self.input_capture.active() { if self.session_lock.active() || self.input_capture.active() {
return false; return false;
} }
let Some(surface) = self.seat.get_keyboard().and_then(|keyboard| keyboard.current_focus()) else { let Some(surface) = self
.seat
.get_keyboard()
.and_then(|keyboard| keyboard.current_focus())
else {
return false; return false;
}; };
if self if self
@@ -890,7 +923,12 @@ impl Ferese {
true true
} }
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() if self.session_lock.active()
|| pointer.is_grabbed() || pointer.is_grabbed()
|| !matches!(button, BTN_LEFT | BTN_RIGHT) || !matches!(button, BTN_LEFT | BTN_RIGHT)
@@ -956,7 +994,9 @@ impl Ferese {
I: InputBackend, I: InputBackend,
E: AbsolutePositionEvent<I>, 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)?; let geometry = self.space.output_geometry(output)?;
Some(event.position_transformed(geometry.size) + geometry.loc.to_f64()) Some(event.position_transformed(geometry.size) + geometry.loc.to_f64())
@@ -965,8 +1005,14 @@ impl Ferese {
fn swipe_navigation_blocked(&self) -> bool { fn swipe_navigation_blocked(&self) -> bool {
self.session_lock.active() self.session_lock.active()
|| self.active_shortcuts_inhibitor.is_some() || self.active_shortcuts_inhibitor.is_some()
|| self.seat.get_pointer().is_some_and(|pointer| pointer.is_grabbed()) || self
|| self.seat.get_keyboard().is_some_and(|keyboard| keyboard.is_grabbed()) .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>) { fn switch_relative_workspace(&mut self, next: bool, slide: Option<SwipeDirection>) {
@@ -982,7 +1028,9 @@ impl Ferese {
pub(crate) fn relative_workspace_target(&self, next: bool) -> Option<ferese_core::WorkspaceId> { pub(crate) fn relative_workspace_target(&self, next: bool) -> Option<ferese_core::WorkspaceId> {
let output = self.output_workspaces.focused_output()?; let output = self.output_workspaces.focused_output()?;
let current = self.output_workspaces.active_workspace(output)?; let current = self.output_workspaces.active_workspace(output)?;
let candidates = self.output_workspaces.ordered_workspaces(&self.workspaces, output); let candidates = self
.output_workspaces
.ordered_workspaces(&self.workspaces, output);
let index = candidates.iter().position(|id| *id == current)?; let index = candidates.iter().position(|id| *id == current)?;
let neighbor = if next { let neighbor = if next {
index.checked_add(1)? index.checked_add(1)?
@@ -1009,7 +1057,9 @@ impl Ferese {
if self.session_lock.active() { if self.session_lock.active() {
// Repeats retain press ownership and never run configurable commands // Repeats retain press ownership and never run configurable commands
// on the lock screen, including keys held before locking. // on the lock screen, including keys held before locking.
if let Some(step) = raw_symbols.iter().find_map(|symbol| locked_brightness_step(*symbol)) if let Some(step) = raw_symbols
.iter()
.find_map(|symbol| locked_brightness_step(*symbol))
&& let Some(child) = spawn_client( && let Some(child) = spawn_client(
self, self,
["brightnessctl", "set", step], ["brightnessctl", "set", step],
@@ -1087,7 +1137,8 @@ impl Ferese {
{ {
// The client may enable OnDemand only after receiving this // The client may enable OnDemand only after receiving this
// opening press. Touch does not establish pointer focus. // opening press. Touch does not establish pointer focus.
self.pending_layer_focus = Some((layer.wl_surface().clone(), std::time::Instant::now())); self.pending_layer_focus =
Some((layer.wl_surface().clone(), std::time::Instant::now()));
} }
return; return;
} }
@@ -1183,7 +1234,11 @@ 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() { if self.session_lock.active() {
return; return;
} }
@@ -1193,7 +1248,10 @@ impl Ferese {
} }
// Keep the explicitly selected window focused until Overview's exit // Keep the explicitly selected window focused until Overview's exit
// animation settles. Moving previews must not steal focus on pointer jitter. // 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; return;
} }
if self.layer_under(position).is_some() { if self.layer_under(position).is_some() {
@@ -1323,9 +1381,11 @@ impl Ferese {
BindingAction::None => {} BindingAction::None => {}
BindingAction::BindingMode(name) => self.set_binding_mode(&name), BindingAction::BindingMode(name) => self.set_binding_mode(&name),
BindingAction::Spawn(argv) => { BindingAction::Spawn(argv) => {
if let Some(child) = if let Some(child) = spawn_client(
spawn_client(self, argv.iter(), crate::private_client::ClientCapabilities::default()) self,
{ argv.iter(),
crate::private_client::ClientCapabilities::default(),
) {
crate::process::watch_client_exit(&self.loop_handle, child); crate::process::watch_client_exit(&self.loop_handle, child);
} }
} }
@@ -1337,7 +1397,9 @@ impl Ferese {
BindingAction::FocusMru(reverse) => self.cycle_focus(reverse, false), BindingAction::FocusMru(reverse) => self.cycle_focus(reverse, false),
BindingAction::Move(direction) => self.move_direction(direction), BindingAction::Move(direction) => self.move_direction(direction),
BindingAction::Resize(direction) => self.resize_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::WorkspaceBackAndForth => self.workspace_back_and_forth(), BindingAction::WorkspaceBackAndForth => self.workspace_back_and_forth(),
BindingAction::SwitchWorkspace(workspace) => { BindingAction::SwitchWorkspace(workspace) => {
self.switch_workspace_from_binding(u32::from(workspace)); self.switch_workspace_from_binding(u32::from(workspace));
@@ -1359,7 +1421,10 @@ impl Ferese {
BindingAction::ToggleFloating => self.toggle_focused_floating(), BindingAction::ToggleFloating => self.toggle_focused_floating(),
BindingAction::ToggleOverview => self.toggle_overview(), BindingAction::ToggleOverview => self.toggle_overview(),
BindingAction::Media(action) => { BindingAction::Media(action) => {
if let Err(error) = self.media_engine.action(serde_json::json!({"action": action}), None) { if let Err(error) = self
.media_engine
.action(serde_json::json!({"action": action}), None)
{
tracing::debug!(%error, "media action unavailable"); tracing::debug!(%error, "media action unavailable");
} }
} }
@@ -1474,17 +1539,25 @@ mod tests {
#[test] #[test]
fn binding_commands_are_reaped_while_the_compositor_runs() { fn binding_commands_are_reaped_while_the_compositor_runs() {
if !crate::startup_tests::private_runtime("input::tests::binding_commands_are_reaped_while_the_compositor_runs") if !crate::startup_tests::private_runtime(
{ "input::tests::binding_commands_are_reaped_while_the_compositor_runs",
) {
return; return;
} }
let mut events = smithay::reexports::calloop::EventLoop::try_new().unwrap(); let mut events = smithay::reexports::calloop::EventLoop::try_new().unwrap();
let mut state = crate::startup_tests::state(&mut events); let mut state = crate::startup_tests::state(&mut events);
let children_path = format!("/proc/self/task/{}/children", unsafe { libc::gettid() }); let children_path = format!("/proc/self/task/{}/children", unsafe { libc::gettid() });
assert!(std::fs::read_to_string(&children_path).unwrap().trim().is_empty()); assert!(
std::fs::read_to_string(&children_path)
.unwrap()
.trim()
.is_empty()
);
for _ in 0..24 { for _ in 0..24 {
state.execute_binding(crate::config::BindingAction::Spawn(vec!["/bin/true".into()].into())); state.execute_binding(crate::config::BindingAction::Spawn(
vec!["/bin/true".into()].into(),
));
} }
let children: Vec<i32> = std::fs::read_to_string(&children_path) let children: Vec<i32> = std::fs::read_to_string(&children_path)
@@ -1494,8 +1567,15 @@ mod tests {
.collect(); .collect();
assert_eq!(children.len(), 24); assert_eq!(children.len(), 24);
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(3); let deadline = std::time::Instant::now() + std::time::Duration::from_secs(3);
while !std::fs::read_to_string(&children_path).unwrap().trim().is_empty() { while !std::fs::read_to_string(&children_path)
assert!(std::time::Instant::now() < deadline, "binding children were not reaped"); .unwrap()
.trim()
.is_empty()
{
assert!(
std::time::Instant::now() < deadline,
"binding children were not reaped"
);
events events
.dispatch(Some(std::time::Duration::from_millis(20)), &mut state) .dispatch(Some(std::time::Duration::from_millis(20)), &mut state)
.unwrap(); .unwrap();
@@ -1504,8 +1584,14 @@ mod tests {
for pid in children { for pid in children {
let mut status = 0; let mut status = 0;
assert_eq!(unsafe { libc::waitpid(pid, &mut status, libc::WNOHANG) }, -1); assert_eq!(
assert_eq!(std::io::Error::last_os_error().raw_os_error(), Some(libc::ECHILD)); unsafe { libc::waitpid(pid, &mut status, libc::WNOHANG) },
-1
);
assert_eq!(
std::io::Error::last_os_error().raw_os_error(),
Some(libc::ECHILD)
);
} }
} }
+25 -7
View File
@@ -60,7 +60,11 @@ pub(crate) fn key(state: &mut Ferese, code: u32, pressed: bool) {
state.process_input_event::<TestInput>(InputEvent::Keyboard { state.process_input_event::<TestInput>(InputEvent::Keyboard {
event: TestKey( event: TestKey(
Keycode::new(code), Keycode::new(code),
if pressed { KeyState::Pressed } else { KeyState::Released }, if pressed {
KeyState::Pressed
} else {
KeyState::Released
},
), ),
}); });
} }
@@ -78,7 +82,10 @@ fn state(events: &mut EventLoop<'static, Ferese>) -> Ferese {
config.wallpaper.path = None; config.wallpaper.path = None;
let mut state = Ferese::new(events, Display::new().unwrap(), config).unwrap(); let mut state = Ferese::new(events, Display::new().unwrap(), config).unwrap();
state.config_source = Some(SOURCE.into()); state.config_source = Some(SOURCE.into());
let mut sections = ferese_config::Document::parse(SOURCE).unwrap().value().clone(); let mut sections = ferese_config::Document::parse(SOURCE)
.unwrap()
.value()
.clone();
sections.as_object_mut().unwrap().remove("theme"); sections.as_object_mut().unwrap().remove("theme");
state.config_sections = Some(sections); state.config_sections = Some(sections);
state state
@@ -149,7 +156,11 @@ fn reload_transitions_cancel_modes_without_losing_key_ownership() {
let mut state = state(&mut events); let mut state = state(&mut events);
state.set_binding_mode("resize"); state.set_binding_mode("resize");
key(&mut state, 43, true); key(&mut state, 43, true);
assert!(state.reload_config_source("binding-mode default {}".into()).is_err()); assert!(
state
.reload_config_source("binding-mode default {}".into())
.is_err()
);
assert_eq!(state.binding_modes.active(), Some("resize")); assert_eq!(state.binding_modes.active(), Some("resize"));
assert!(state.intercepted_keys.contains(&Keycode::new(43))); assert!(state.intercepted_keys.contains(&Keycode::new(43)));
state state
@@ -163,7 +174,11 @@ fn reload_transitions_cancel_modes_without_losing_key_ownership() {
state state
.reload_config_source(format!("{SOURCE}\ninput {{ repeat-rate 30; }}")) .reload_config_source(format!("{SOURCE}\ninput {{ repeat-rate 30; }}"))
.unwrap(); .unwrap();
assert_eq!(state.binding_modes.active(), None, "input reload exits mode"); assert_eq!(
state.binding_modes.active(),
None,
"input reload exits mode"
);
state.set_binding_mode("resize"); state.set_binding_mode("resize");
state state
.reload_config_source("binding-mode launch { binding t spawn terminal; }".into()) .reload_config_source("binding-mode launch { binding t spawn terminal; }".into())
@@ -176,8 +191,9 @@ fn reload_transitions_cancel_modes_without_losing_key_ownership() {
#[test] #[test]
fn escape_exits_with_every_modifier_combination() { fn escape_exits_with_every_modifier_combination() {
if !crate::startup_tests::private_runtime("input::binding_mode_tests::escape_exits_with_every_modifier_combination") if !crate::startup_tests::private_runtime(
{ "input::binding_mode_tests::escape_exits_with_every_modifier_combination",
) {
return; return;
} }
let mut events = EventLoop::try_new().unwrap(); let mut events = EventLoop::try_new().unwrap();
@@ -220,7 +236,9 @@ fn input_capture_exits_modes_and_prevents_reentry() {
y: 0, y: 0,
}]); }]);
let id = state.input_capture.register(1, 3).unwrap(); let id = state.input_capture.register(1, 3).unwrap();
let zone_set = state.input_capture.zones(id).unwrap()["zone_set"].as_u64().unwrap() as u32; let zone_set = state.input_capture.zones(id).unwrap()["zone_set"]
.as_u64()
.unwrap() as u32;
state state
.input_capture .input_capture
.set_barriers( .set_barriers(
+82 -21
View File
@@ -1,6 +1,8 @@
use super::*; use super::*;
use crate::state::ClientState; use crate::state::ClientState;
use crate::state::{dispatch_test_client as dispatch, test_client_request as request, test_window as window}; use crate::state::{
dispatch_test_client as dispatch, test_client_request as request, test_window as window,
};
use smithay::desktop::{LayerSurface, layer_map_for_output}; use smithay::desktop::{LayerSurface, layer_map_for_output};
use smithay::output::{Mode, Output, PhysicalProperties, Subpixel}; use smithay::output::{Mode, Output, PhysicalProperties, Subpixel};
use smithay::reexports::calloop::EventLoop; use smithay::reexports::calloop::EventLoop;
@@ -11,7 +13,11 @@ use std::os::fd::AsRawFd;
use std::os::unix::net::UnixStream; use std::os::unix::net::UnixStream;
use std::sync::Arc; use std::sync::Arc;
fn layer(state: &mut Ferese, events: &mut EventLoop<'static, Ferese>, output: &Output) -> (LayerSurface, UnixStream) { fn layer(
state: &mut Ferese,
events: &mut EventLoop<'static, Ferese>,
output: &Output,
) -> (LayerSurface, UnixStream) {
layer_with_policy(state, events, output, 0, 2) layer_with_policy(state, events, output, 0, 2)
} }
@@ -81,7 +87,9 @@ fn layer_with_policy(
let object = u32::from_ne_bytes(bytes[offset..offset + 4].try_into().unwrap()); let object = u32::from_ne_bytes(bytes[offset..offset + 4].try_into().unwrap());
let header = u32::from_ne_bytes(bytes[offset + 4..offset + 8].try_into().unwrap()); let header = u32::from_ne_bytes(bytes[offset + 4..offset + 8].try_into().unwrap());
if object == 3 && header & 0xffff == 0 { if object == 3 && header & 0xffff == 0 {
serial = Some(u32::from_ne_bytes(bytes[offset + 8..offset + 12].try_into().unwrap())); serial = Some(u32::from_ne_bytes(
bytes[offset + 8..offset + 12].try_into().unwrap(),
));
} }
offset += (header >> 16) as usize; offset += (header >> 16) as usize;
} }
@@ -93,7 +101,8 @@ fn layer_with_policy(
"acknowledging configure without mapping must not acquire focus" "acknowledging configure without mapping must not acquire focus"
); );
let mut file = tempfile::tempfile().unwrap(); let mut file = tempfile::tempfile().unwrap();
file.write_all(&0xff112233u32.to_ne_bytes().repeat(64 * 48)).unwrap(); file.write_all(&0xff112233u32.to_ne_bytes().repeat(64 * 48))
.unwrap();
request( request(
&mut wire, &mut wire,
shm.id().protocol_id(), shm.id().protocol_id(),
@@ -198,7 +207,11 @@ fn on_demand_layer_focus_follows_opening_and_outside_clicks() {
}, },
); );
state.focus_window_at(outside, SERIAL_COUNTER.next_serial(), true); state.focus_window_at(outside, SERIAL_COUNTER.next_serial(), true);
assert_eq!(keyboard.current_focus(), None, "empty-desktop clicks dismiss the panel"); assert_eq!(
keyboard.current_focus(),
None,
"empty-desktop clicks dismiss the panel"
);
request(&mut wire, 2, 6, &[], None); request(&mut wire, 2, 6, &[], None);
dispatch(&mut events, &mut state); dispatch(&mut events, &mut state);
assert_eq!( assert_eq!(
@@ -225,7 +238,9 @@ fn on_demand_layer_focus_follows_opening_and_outside_clicks() {
let (second, _second_wire) = window(&mut state, &mut events, 0xff334455); let (second, _second_wire) = window(&mut state, &mut events, 0xff334455);
let first_id = state.windows.ids()[&first]; let first_id = state.windows.ids()[&first];
assert!(state.activate_managed_window(first_id)); assert!(state.activate_managed_window(first_id));
let second_bounds = state.presented_window_rect(state.windows.ids()[&second]).unwrap(); let second_bounds = state
.presented_window_rect(state.windows.ids()[&second])
.unwrap();
let on_window = Point::from((second_bounds.x + 20., second_bounds.y + 20.)); let on_window = Point::from((second_bounds.x + 20., second_bounds.y + 20.));
state.input_settings.focus_follows_mouse = true; state.input_settings.focus_follows_mouse = true;
keyboard.set_focus( keyboard.set_focus(
@@ -297,7 +312,12 @@ fn fixture_with_animations(animations: bool) -> (Ferese, EventLoop<'static, Fere
(state, events, output) (state, events, output)
} }
fn commit_policy(state: &mut Ferese, events: &mut EventLoop<'static, Ferese>, wire: &mut UnixStream, keyboard: u32) { fn commit_policy(
state: &mut Ferese,
events: &mut EventLoop<'static, Ferese>,
wire: &mut UnixStream,
keyboard: u32,
) {
request(wire, 3, 4, &[keyboard], None); request(wire, 3, 4, &[keyboard], None);
request(wire, 2, 6, &[], None); request(wire, 2, 6, &[], None);
dispatch(events, state); dispatch(events, state);
@@ -345,17 +365,24 @@ fn null_buffer_commit_revokes_layer_focus_without_destroying_role() {
request(&mut wire, 2, 1, &[0, 0, 0], None); // wl_surface.attach(NULL). request(&mut wire, 2, 1, &[0, 0, 0], None); // wl_surface.attach(NULL).
request(&mut wire, 2, 6, &[], None); request(&mut wire, 2, 6, &[], None);
dispatch(&mut events, &mut state); dispatch(&mut events, &mut state);
assert!(panel.layer_surface().alive(), "the layer role survives unmapping"); assert!(
panel.layer_surface().alive(),
"the layer role survives unmapping"
);
assert!( assert!(
state state
.space .space
.layer_for_surface(panel.wl_surface(), smithay::desktop::WindowSurfaceType::TOPLEVEL) .layer_for_surface(
panel.wl_surface(),
smithay::desktop::WindowSurfaceType::TOPLEVEL
)
.is_some() .is_some()
); );
assert!( assert!(
smithay::backend::renderer::utils::with_renderer_surface_state(panel.wl_surface(), |s| s smithay::backend::renderer::utils::with_renderer_surface_state(
.buffer() panel.wl_surface(),
.is_none()) |s| s.buffer().is_none()
)
.unwrap() .unwrap()
); );
assert_eq!( assert_eq!(
@@ -392,7 +419,11 @@ fn null_buffer_commit_revokes_layer_focus_without_destroying_role() {
); );
request(&mut wire, 3, 7, &[], None); // Destroy the role. request(&mut wire, 3, 7, &[], None); // Destroy the role.
dispatch(&mut events, &mut state); dispatch(&mut events, &mut state);
assert_eq!(keyboard.current_focus(), fallback, "destruction restores window focus"); assert_eq!(
keyboard.current_focus(),
fallback,
"destruction restores window focus"
);
} }
} }
@@ -410,7 +441,9 @@ fn ack_latest_configure(wire: &mut UnixStream) {
let object = u32::from_ne_bytes(bytes[offset..offset + 4].try_into().unwrap()); let object = u32::from_ne_bytes(bytes[offset..offset + 4].try_into().unwrap());
let header = u32::from_ne_bytes(bytes[offset + 4..offset + 8].try_into().unwrap()); let header = u32::from_ne_bytes(bytes[offset + 4..offset + 8].try_into().unwrap());
if object == 3 && header & 0xffff == 0 { if object == 3 && header & 0xffff == 0 {
serial = Some(u32::from_ne_bytes(bytes[offset + 8..offset + 12].try_into().unwrap())); serial = Some(u32::from_ne_bytes(
bytes[offset + 8..offset + 12].try_into().unwrap(),
));
} }
offset += (header >> 16) as usize; offset += (header >> 16) as usize;
} }
@@ -495,7 +528,11 @@ fn layer_mapping_commits_do_not_override_session_lock() {
request(&mut wire, 2, 6, &[], None); request(&mut wire, 2, 6, &[], None);
dispatch(&mut events, &mut state); dispatch(&mut events, &mut state);
remap_layer(&mut state, &mut events, &mut wire, 1, 3); remap_layer(&mut state, &mut events, &mut wire, 1, 3);
assert_eq!(keyboard.current_focus(), None, "layer remapping cannot bypass the lock"); assert_eq!(
keyboard.current_focus(),
None,
"layer remapping cannot bypass the lock"
);
request(&mut wire, 3, 7, &[], None); request(&mut wire, 3, 7, &[], None);
dispatch(&mut events, &mut state); dispatch(&mut events, &mut state);
assert_eq!( assert_eq!(
@@ -631,7 +668,11 @@ fn valid_layer_popup_grab_survives_commits_but_not_parent_unmapping() {
) )
.unwrap(); .unwrap();
let shell = client let shell = client
.create_resource::<XdgWmBase, XdgWmBaseUserData, Ferese>(&state.display_handle, 6, Default::default()) .create_resource::<XdgWmBase, XdgWmBaseUserData, Ferese>(
&state.display_handle,
6,
Default::default(),
)
.unwrap(); .unwrap();
request(&mut wire, shell.id().protocol_id(), 1, &[6], None); // Positioner. request(&mut wire, shell.id().protocol_id(), 1, &[6], None); // Positioner.
request(&mut wire, 6, 1, &[32, 24], None); request(&mut wire, 6, 1, &[32, 24], None);
@@ -657,7 +698,12 @@ fn valid_layer_popup_grab_survives_commits_but_not_parent_unmapping() {
let seat = state.seat.clone(); let seat = state.seat.clone();
let grab = state let grab = state
.popups .popups
.grab_popup(top.wl_surface().clone(), PopupKind::Xdg(popup.clone()), &seat, serial) .grab_popup(
top.wl_surface().clone(),
PopupKind::Xdg(popup.clone()),
&seat,
serial,
)
.unwrap(); .unwrap();
let keyboard = state.seat.get_keyboard().unwrap(); let keyboard = state.seat.get_keyboard().unwrap();
keyboard.set_grab(&mut state, PopupKeyboardGrab::new(&grab), serial); keyboard.set_grab(&mut state, PopupKeyboardGrab::new(&grab), serial);
@@ -680,7 +726,10 @@ fn valid_layer_popup_grab_survives_commits_but_not_parent_unmapping() {
request(&mut wire, 2, 1, &[0, 0, 0], None); request(&mut wire, 2, 1, &[0, 0, 0], None);
request(&mut wire, 2, 6, &[], None); request(&mut wire, 2, 6, &[], None);
dispatch(&mut events, &mut state); dispatch(&mut events, &mut state);
assert!(!keyboard.is_grabbed(), "unmapping revokes the parent popup grab"); assert!(
!keyboard.is_grabbed(),
"unmapping revokes the parent popup grab"
);
assert_ne!(keyboard.current_focus(), popup_focus); assert_ne!(keyboard.current_focus(), popup_focus);
assert_ne!(keyboard.current_focus(), Some(top.wl_surface().clone())); assert_ne!(keyboard.current_focus(), Some(top.wl_surface().clone()));
} }
@@ -699,8 +748,16 @@ fn fullscreen_layer_eligibility_is_per_output_and_respects_overview() {
state.set_window_fullscreen(id, true); state.set_window_fullscreen(id, true);
let (top, _top_wire) = layer_with_policy(&mut state, &mut events, &output, 1, 2); let (top, _top_wire) = layer_with_policy(&mut state, &mut events, &output, 1, 2);
assert!(!state.layer_keyboard_eligible(&top)); assert!(!state.layer_keyboard_eligible(&top));
assert!(!state.layer_visible_for_frame(&output, smithay::wayland::shell::wlr_layer::Layer::Top, false)); assert!(!state.layer_visible_for_frame(
assert!(state.layer_visible_for_frame(&output, smithay::wayland::shell::wlr_layer::Layer::Top, true)); &output,
smithay::wayland::shell::wlr_layer::Layer::Top,
false
));
assert!(state.layer_visible_for_frame(
&output,
smithay::wayland::shell::wlr_layer::Layer::Top,
true
));
state.set_overview_active(true); state.set_overview_active(true);
assert!(state.layer_keyboard_eligible(&top)); assert!(state.layer_keyboard_eligible(&top));
assert_eq!( assert_eq!(
@@ -837,7 +894,11 @@ fn touch_opened_on_demand_layer_does_not_need_pointer_focus() {
}, },
); );
state.focus_window_at(outside, SERIAL_COUNTER.next_serial(), true); state.focus_window_at(outside, SERIAL_COUNTER.next_serial(), true);
assert_eq!(keyboard.current_focus(), None, "empty-desktop clicks dismiss the panel"); assert_eq!(
keyboard.current_focus(),
None,
"empty-desktop clicks dismiss the panel"
);
request(&mut wire, 2, 6, &[], None); request(&mut wire, 2, 6, &[], None);
dispatch(&mut events, &mut state); dispatch(&mut events, &mut state);
assert_eq!( assert_eq!(
+77 -18
View File
@@ -89,14 +89,24 @@ fn edge(barrier: &Barrier, zones: &[Zone]) -> Option<Edge> {
} }
match found { match found {
Edge::Top | Edge::Bottom => { 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 >= f64::from(other.y)
&& outside < other.bottom() && outside < other.bottom()
&& x1 < other.right() && x1 < other.right()
&& x2 + 1.0 > f64::from(other.x) && x2 + 1.0 > f64::from(other.x)
} }
Edge::Left | Edge::Right => { 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 >= f64::from(other.x)
&& outside < other.right() && outside < other.right()
&& y1 < other.bottom() && y1 < other.bottom()
@@ -174,7 +184,10 @@ impl InputCapture {
if self.sessions.len() >= MAX_SESSIONS || capabilities == 0 || capabilities & !3 != 0 { if self.sessions.len() >= MAX_SESSIONS || capabilities == 0 || capabilities & !3 != 0 {
return Err("Unsupported capabilities or input capture session limit reached".into()); 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; let id = self.next_session;
self.sessions.insert( self.sessions.insert(
id, id,
@@ -193,15 +206,24 @@ impl InputCapture {
pub(crate) fn zones(&self, id: u64) -> Result<Value, String> { pub(crate) fn zones(&self, id: u64) -> Result<Value, String> {
self.session(id)?; self.session(id)?;
Ok(json!({"zone_set":self.zone_set, "zones":self.zones.iter().map(|zone| Ok(
(zone.width, zone.height, zone.x, zone.y)).collect::<Vec<_>>()})) 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> { 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)?; self.session(id)?;
if barriers.len() > MAX_BARRIERS { if barriers.len() > MAX_BARRIERS {
return Err("Too many pointer barriers".into()); return Err("Too many pointer barriers".into());
@@ -229,7 +251,10 @@ impl InputCapture {
} }
pub(crate) fn enable(&mut self, id: u64) -> Result<(), String> { 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 { if session.closed {
return Err("Input capture session is closed".into()); return Err("Input capture session is closed".into());
} }
@@ -255,7 +280,11 @@ impl InputCapture {
if self.active() { if self.active() {
return true; 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; return false;
} }
let hit = self let hit = self
@@ -340,7 +369,10 @@ impl InputCapture {
self.restore_focus = true; self.restore_focus = true;
let session = self.sessions.get_mut(&id).unwrap(); let session = self.sessions.get_mut(&id).unwrap();
let activation = session.activation.take(); 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(); self.flush();
} }
Ok(()) Ok(())
@@ -365,10 +397,18 @@ impl InputCapture {
} }
pub(crate) fn valid_position(&self, point: (f64, f64)) -> bool { 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) if !self.sessions.contains_key(&id)
|| self.waiters.len() >= MAX_SESSIONS || self.waiters.len() >= MAX_SESSIONS
|| self.waiters.values().any(|(_, token, _)| *token == id) || self.waiters.values().any(|(_, token, _)| *token == id)
@@ -479,10 +519,19 @@ impl crate::Ferese {
&& !self.overview.is_presenting() && !self.overview.is_presenting()
&& !pointer.is_grabbed() && !pointer.is_grabbed()
&& !self.swipe.active() && !self.swipe.active()
&& self.seat.get_keyboard().is_none_or(|keyboard| !keyboard.is_grabbed()) && self
&& self.seat.get_touch().is_none_or(|touch| !touch.is_grabbed()) .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(); && !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; return false;
} }
let delta = if absolute { let delta = if absolute {
@@ -553,7 +602,10 @@ mod tests {
[3840, 0, 3840, 1079], [3840, 0, 3840, 1079],
[0, 0, 0, 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 [ for position in [
[1920, 0, 1920, 1079], [1920, 0, 1920, 1079],
@@ -562,7 +614,10 @@ mod tests {
[50, 50, 50, 200], [50, 50, 50, 200],
[0, 5, 0, 4], [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 { let barrier = Barrier {
id: 1, id: 1,
@@ -651,7 +706,11 @@ mod tests {
capture.enable(id).unwrap(); capture.enable(id).unwrap();
assert!(capture.motion((3839.0, 50.0), (3900.0, 50.0), true)); assert!(capture.motion((3839.0, 50.0), (3900.0, 50.0), true));
let activation = capture.sessions[&id].activation.unwrap(); 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(); capture.release(id, Some(activation)).unwrap();
assert!(capture.sessions[&id].enabled); assert!(capture.sessions[&id].enabled);
capture.update_zones(vec![zones()[0]]); capture.update_zones(vec![zones()[0]]);
+544 -340
View File
File diff suppressed because it is too large. Load diff
+38 -9
View File
@@ -25,7 +25,9 @@ impl VerifiedOwner {
let mut next = self.owner.pid; let mut next = self.owner.pid;
let mut child = None; let mut child = None;
for _ in 0..16 { for _ in 0..16 {
let Ok((birth, parent)) = identity(next) else { break }; let Ok((birth, parent)) = identity(next) else {
break;
};
if next == self.owner.pid && birth != self.owner.birth { if next == self.owner.pid && birth != self.owner.birth {
break; break;
} }
@@ -59,7 +61,10 @@ impl VerifiedOwner {
pub(crate) struct Monitor(Arc<(Mutex<BTreeMap<Owner, OwnedFd>>, Condvar)>); pub(crate) struct Monitor(Arc<(Mutex<BTreeMap<Owner, OwnedFd>>, Condvar)>);
impl Monitor { impl Monitor {
pub fn new(sender: channel::SyncSender<IpcEvent>) -> Self { pub fn new(sender: channel::SyncSender<IpcEvent>) -> Self {
let shared = Arc::new((Mutex::new(BTreeMap::<Owner, OwnedFd>::new()), Condvar::new())); let shared = Arc::new((
Mutex::new(BTreeMap::<Owner, OwnedFd>::new()),
Condvar::new(),
));
let worker = shared.clone(); let worker = shared.clone();
std::thread::Builder::new() std::thread::Builder::new()
.name("ferese-activity-owners".into()) .name("ferese-activity-owners".into())
@@ -104,7 +109,11 @@ impl Monitor {
.expect("activity owners") .expect("activity owners")
.retain(|owner, _| owners.contains(owner)); .retain(|owner, _| owners.contains(owner));
} }
pub fn verify(&self, stream: &UnixStream, parent: bool) -> io::Result<(VerifiedOwner, Vec<String>)> { pub fn verify(
&self,
stream: &UnixStream,
parent: bool,
) -> io::Result<(VerifiedOwner, Vec<String>)> {
let peer = credentials(stream)?; let peer = credentials(stream)?;
let peer_pid = u32::try_from(peer.pid).map_err(|_| io::Error::other("invalid peer PID"))?; let peer_pid = u32::try_from(peer.pid).map_err(|_| io::Error::other("invalid peer PID"))?;
let peer_identity = identity(peer_pid)?; let peer_identity = identity(peer_pid)?;
@@ -115,7 +124,10 @@ impl Monitor {
let pid = if parent { peer_identity.1 } else { peer_pid }; let pid = if parent { peer_identity.1 } else { peer_pid };
let (birth, _) = identity(pid)?; let (birth, _) = identity(pid)?;
let fd = pidfd(pid)?; let fd = pidfd(pid)?;
if identity(pid)?.0 != birth || exited(&fd) || (parent && identity(peer_pid)? != peer_identity) { if identity(pid)?.0 != birth
|| exited(&fd)
|| (parent && identity(peer_pid)? != peer_identity)
{
return Err(io::Error::other("owner identity changed")); return Err(io::Error::other("owner identity changed"));
} }
let metadata = std::fs::metadata(format!("/proc/{pid}"))?; let metadata = std::fs::metadata(format!("/proc/{pid}"))?;
@@ -224,7 +236,11 @@ fn parse_stat(stat: &str) -> io::Result<(u64, u32)> {
Ok((birth, parent)) Ok((birth, parent))
} }
fn credentials(stream: &UnixStream) -> io::Result<libc::ucred> { fn credentials(stream: &UnixStream) -> io::Result<libc::ucred> {
let mut credentials = libc::ucred { pid: 0, uid: 0, gid: 0 }; let mut credentials = libc::ucred {
pid: 0,
uid: 0,
gid: 0,
};
let mut len = std::mem::size_of::<libc::ucred>() as libc::socklen_t; let mut len = std::mem::size_of::<libc::ucred>() as libc::socklen_t;
// SAFETY: both output buffers have the sizes expected by SO_PEERCRED. // SAFETY: both output buffers have the sizes expected by SO_PEERCRED.
if unsafe { if unsafe {
@@ -274,7 +290,10 @@ mod tests {
use super::*; use super::*;
#[test] #[test]
fn captured_ancestry_stops_at_an_exited_process() { fn captured_ancestry_stops_at_an_exited_process() {
let mut child = std::process::Command::new("sleep").arg("30").spawn().unwrap(); let mut child = std::process::Command::new("sleep")
.arg("30")
.spawn()
.unwrap();
let (owner, fd, _) = inspect(child.id()).unwrap(); let (owner, fd, _) = inspect(child.id()).unwrap();
let mut verified = VerifiedOwner { let mut verified = VerifiedOwner {
owner, owner,
@@ -283,7 +302,11 @@ mod tests {
}; };
verified.capture_ancestors(); verified.capture_ancestors();
assert_eq!(verified.ancestor_pids().next(), Some(child.id())); assert_eq!(verified.ancestor_pids().next(), Some(child.id()));
assert!(verified.ancestor_pids().any(|pid| pid == std::process::id())); assert!(
verified
.ancestor_pids()
.any(|pid| pid == std::process::id())
);
child.kill().unwrap(); child.kill().unwrap();
child.wait().unwrap(); child.wait().unwrap();
assert!(verified.ancestor_pids().next().is_none()); assert!(verified.ancestor_pids().next().is_none());
@@ -334,7 +357,10 @@ mod tests {
fn owner_that_exits_in_the_dispatch_queue_is_rejected() { fn owner_that_exits_in_the_dispatch_queue_is_rejected() {
let (sender, _receiver) = channel::sync_channel(16); let (sender, _receiver) = channel::sync_channel(16);
let monitor = Monitor::new(sender); let monitor = Monitor::new(sender);
let mut child = std::process::Command::new("sleep").arg("30").spawn().unwrap(); let mut child = std::process::Command::new("sleep")
.arg("30")
.spawn()
.unwrap();
let (owner, fd, _) = inspect(child.id()).unwrap(); let (owner, fd, _) = inspect(child.id()).unwrap();
child.kill().unwrap(); child.kill().unwrap();
child.wait().unwrap(); child.wait().unwrap();
@@ -370,7 +396,10 @@ mod tests {
assert_eq!(peer.owner.pid, std::process::id()); assert_eq!(peer.owner.pid, std::process::id());
let (parent, _) = monitor.verify(&left, true).unwrap(); let (parent, _) = monitor.verify(&left, true).unwrap();
assert_eq!(parent.owner.pid, unsafe { libc::getppid() } as u32); assert_eq!(parent.owner.pid, unsafe { libc::getppid() } as u32);
let mut child = std::process::Command::new("sleep").arg("0.05").spawn().unwrap(); let mut child = std::process::Command::new("sleep")
.arg("0.05")
.spawn()
.unwrap();
let fd = pidfd(child.id()).unwrap(); let fd = pidfd(child.id()).unwrap();
assert!(!exited(&fd)); assert!(!exited(&fd));
child.wait().unwrap(); child.wait().unwrap();
+16 -5
View File
@@ -41,7 +41,10 @@ impl Write for DeadlineIo<'_> {
} }
} }
pub(super) fn read_request(stream: &UnixStream, idle: Option<Duration>) -> Result<Request, FrameError> { pub(super) fn read_request(
stream: &UnixStream,
idle: Option<Duration>,
) -> Result<Request, FrameError> {
read_request_with_budget(stream, idle, FRAME_TIMEOUT) read_request_with_budget(stream, idle, FRAME_TIMEOUT)
} }
@@ -60,7 +63,10 @@ fn read_request_with_budget(
ferese_ipc::read_frame(&mut first.as_slice().chain(reader)) ferese_ipc::read_frame(&mut first.as_slice().chain(reader))
} }
pub(super) fn write_response(stream: &UnixStream, value: &impl serde::Serialize) -> Result<(), FrameError> { pub(super) fn write_response(
stream: &UnixStream,
value: &impl serde::Serialize,
) -> Result<(), FrameError> {
// Tests may install a tighter socket timeout. // Tests may install a tighter socket timeout.
let previous = stream.write_timeout()?; let previous = stream.write_timeout()?;
let budget = previous.unwrap_or(WRITE_TIMEOUT).min(WRITE_TIMEOUT); let budget = previous.unwrap_or(WRITE_TIMEOUT).min(WRITE_TIMEOUT);
@@ -88,7 +94,8 @@ mod tests {
assert!(read_request_with_budget(&server, Some(short), short).is_err()); assert!(read_request_with_budget(&server, Some(short), short).is_err());
let (mut client, server) = UnixStream::pair().unwrap(); let (mut client, server) = UnixStream::pair().unwrap();
let worker = std::thread::spawn(move || read_request_with_budget(&server, None, short).unwrap()); let worker =
std::thread::spawn(move || read_request_with_budget(&server, None, short).unwrap());
std::thread::sleep(short * 3); std::thread::sleep(short * 3);
let request = Request { let request = Request {
version: 1, version: 1,
@@ -112,8 +119,12 @@ mod tests {
std::thread::sleep(Duration::from_millis(30)); std::thread::sleep(Duration::from_millis(30));
} }
}); });
let error = let error = read_request_with_budget(
read_request_with_budget(&server, Some(Duration::from_secs(1)), Duration::from_millis(70)).unwrap_err(); &server,
Some(Duration::from_secs(1)),
Duration::from_millis(70),
)
.unwrap_err();
assert!( assert!(
matches!(error, FrameError::Io(error) if matches!(error.kind(), io::ErrorKind::TimedOut | io::ErrorKind::WouldBlock)) matches!(error, FrameError::Io(error) if matches!(error.kind(), io::ErrorKind::TimedOut | io::ErrorKind::WouldBlock))
); );
+65 -21
View File
@@ -21,11 +21,21 @@ pub(crate) struct Subscribers {
} }
impl Subscribers { impl Subscribers {
pub(crate) fn subscribe(&mut self, owner: u64, sender: SyncSender<Arc<Event>>) -> Result<(), &'static str> { pub(crate) fn subscribe(
&mut self,
owner: u64,
sender: SyncSender<Arc<Event>>,
) -> Result<(), &'static str> {
if self.subscribers.len() >= MAX_SUBSCRIBERS { if self.subscribers.len() >= MAX_SUBSCRIBERS {
return Err("Event subscriber limit reached"); return Err("Event subscriber limit reached");
} }
self.subscribers.insert(owner, Subscriber { sender, initial: true }); self.subscribers.insert(
owner,
Subscriber {
sender,
initial: true,
},
);
Ok(()) Ok(())
} }
@@ -47,7 +57,12 @@ impl Subscribers {
.as_ref() .as_ref()
.map(|previous| snapshot.changes_since(previous)) .map(|previous| snapshot.changes_since(previous))
.unwrap_or_default(); .unwrap_or_default();
if changes.is_empty() && !self.subscribers.values().any(|subscriber| subscriber.initial) { if changes.is_empty()
&& !self
.subscribers
.values()
.any(|subscriber| subscriber.initial)
{
return; return;
} }
self.generation = self.generation.wrapping_add(1); self.generation = self.generation.wrapping_add(1);
@@ -65,19 +80,26 @@ impl Subscribers {
if let Some(last) = events.last_mut() { if let Some(last) = events.last_mut() {
Arc::get_mut(last).unwrap().last = true; Arc::get_mut(last).unwrap().last = true;
} }
let initial = self.subscribers.values().any(|subscriber| subscriber.initial).then(|| { let initial = self
Arc::new(Event { .subscribers
version: VERSION, .values()
generation: self.generation, .any(|subscriber| subscriber.initial)
last: true, .then(|| {
change: Change::Snapshot { Arc::new(Event {
desktop: Box::new(snapshot.clone()), version: VERSION,
}, generation: self.generation,
}) last: true,
}); change: Change::Snapshot {
desktop: Box::new(snapshot.clone()),
},
})
});
self.subscribers.retain(|_, subscriber| { self.subscribers.retain(|_, subscriber| {
let delivered = if subscriber.initial { let delivered = if subscriber.initial {
subscriber.sender.try_send(initial.as_ref().unwrap().clone()).is_ok() subscriber
.sender
.try_send(initial.as_ref().unwrap().clone())
.is_ok()
} else { } else {
events events
.iter() .iter()
@@ -137,7 +159,10 @@ mod tests {
hub.subscribe(1, sender).unwrap(); hub.subscribe(1, sender).unwrap();
let mut state = snapshot(); let mut state = snapshot();
hub.publish(state.clone()); hub.publish(state.clone());
assert!(matches!(receive.try_recv().unwrap().change, Change::Snapshot { .. })); assert!(matches!(
receive.try_recv().unwrap().change,
Change::Snapshot { .. }
));
hub.publish(state.clone()); hub.publish(state.clone());
assert!(matches!(receive.try_recv(), Err(TryRecvError::Empty))); assert!(matches!(receive.try_recv(), Err(TryRecvError::Empty)));
state.outputs = json!([{"id": 2}]); state.outputs = json!([{"id": 2}]);
@@ -146,7 +171,11 @@ mod tests {
hub.publish(state); hub.publish(state);
let events = receive.try_iter().collect::<Vec<_>>(); let events = receive.try_iter().collect::<Vec<_>>();
assert_eq!(events.len(), 3); assert_eq!(events.len(), 3);
assert!(events.iter().all(|event| event.generation == events[0].generation)); assert!(
events
.iter()
.all(|event| event.generation == events[0].generation)
);
assert!(!events[0].last && !events[1].last && events[2].last); assert!(!events[0].last && !events[1].last && events[2].last);
} }
@@ -171,7 +200,10 @@ mod tests {
)); ));
assert_eq!(hub.subscribers.len(), 1); assert_eq!(hub.subscribers.len(), 1);
slow_receive.try_recv().unwrap(); slow_receive.try_recv().unwrap();
assert!(matches!(slow_receive.try_recv(), Err(TryRecvError::Disconnected))); assert!(matches!(
slow_receive.try_recv(),
Err(TryRecvError::Disconnected)
));
hub.remove(2); hub.remove(2);
assert!(!hub.active()); assert!(!hub.active());
assert!(hub.previous.is_none()); assert!(hub.previous.is_none());
@@ -209,7 +241,11 @@ mod tests {
hub.publish(state); hub.publish(state);
let events = receive.try_iter().collect::<Vec<_>>(); let events = receive.try_iter().collect::<Vec<_>>();
assert_eq!(events.len(), 7); assert_eq!(events.len(), 7);
assert!(events.iter().all(|event| event.generation == events[0].generation)); assert!(
events
.iter()
.all(|event| event.generation == events[0].generation)
);
assert!(events[..6].iter().all(|event| !event.last)); assert!(events[..6].iter().all(|event| !event.last));
assert!(events[6].last); assert!(events[6].last);
} }
@@ -229,7 +265,8 @@ mod tests {
let mut event_loop = EventLoop::try_new().unwrap(); let mut event_loop = EventLoop::try_new().unwrap();
let mut config = crate::config::Config::default().runtime_config().unwrap(); let mut config = crate::config::Config::default().runtime_config().unwrap();
config.wallpaper.path = None; config.wallpaper.path = None;
let mut state = crate::Ferese::new(&mut event_loop, Display::new().unwrap(), config).unwrap(); let mut state =
crate::Ferese::new(&mut event_loop, Display::new().unwrap(), config).unwrap();
let output = |name: &str, x| DesktopOutput { let output = |name: &str, x| DesktopOutput {
output: Output::new( output: Output::new(
name.into(), name.into(),
@@ -273,7 +310,11 @@ mod tests {
state.publish_ipc_events(); state.publish_ipc_events();
let events = receive.try_iter().collect::<Vec<_>>(); let events = receive.try_iter().collect::<Vec<_>>();
assert!(events.last().unwrap().last); assert!(events.last().unwrap().last);
assert!(events.iter().all(|event| event.generation == events[0].generation)); assert!(
events
.iter()
.all(|event| event.generation == events[0].generation)
);
let final_state = state.ipc_event_snapshot(); let final_state = state.ipc_event_snapshot();
assert_eq!(final_state.outputs.as_array().unwrap().len(), 2); assert_eq!(final_state.outputs.as_array().unwrap().len(), 2);
for workspace in final_state.workspaces.as_array().unwrap() { for workspace in final_state.workspaces.as_array().unwrap() {
@@ -336,7 +377,10 @@ mod tests {
}); });
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(3); let deadline = std::time::Instant::now() + std::time::Duration::from_secs(3);
while completion.try_recv().is_err() { while completion.try_recv().is_err() {
assert!(std::time::Instant::now() < deadline, "subscription worker timed out"); assert!(
std::time::Instant::now() < deadline,
"subscription worker timed out"
);
event_loop event_loop
.dispatch(std::time::Duration::from_millis(10), &mut state) .dispatch(std::time::Duration::from_millis(10), &mut state)
.unwrap(); .unwrap();
+12 -5
View File
@@ -80,8 +80,8 @@ fn main() -> Result<(), Box<dyn Error>> {
return Ok(()); return Ok(());
} }
let launch = let launch = LaunchConfig::from_environment()
LaunchConfig::from_environment().map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, error))?; .map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, error))?;
let config_path = config::config_path(); let config_path = config::config_path();
let startup = config::startup(config_path.as_deref())?; let startup = config::startup(config_path.as_deref())?;
if let Some(error) = &startup.error { if let Some(error) = &startup.error {
@@ -120,7 +120,9 @@ fn main() -> Result<(), Box<dyn Error>> {
value value
}); });
if let Some(error) = startup.error { if let Some(error) = startup.error {
state.theme_engine.startup_failed(config_path.as_deref(), error); state
.theme_engine
.startup_failed(config_path.as_deref(), error);
} }
theme::init(&mut event_loop, &mut state, startup.theme)?; theme::init(&mut event_loop, &mut state, startup.theme)?;
let _resume_monitor = resume::init(&mut event_loop)?; let _resume_monitor = resume::init(&mut event_loop)?;
@@ -201,7 +203,10 @@ fn after_dispatch(state: &mut Ferese) {
}); });
} }
fn after_dispatch_with_redraw(state: &mut Ferese, mut redraw: impl FnMut(&mut Ferese, RedrawRequest<'_>)) { fn after_dispatch_with_redraw(
state: &mut Ferese,
mut redraw: impl FnMut(&mut Ferese, RedrawRequest<'_>),
) {
crate::backends::direct::gamma::sync_night_light(state); crate::backends::direct::gamma::sync_night_light(state);
state.process_pending_dmabuf_imports(); state.process_pending_dmabuf_imports();
state.update_named_cursor(std::time::Instant::now()); state.update_named_cursor(std::time::Instant::now());
@@ -213,7 +218,9 @@ fn after_dispatch_with_redraw(state: &mut Ferese, mut redraw: impl FnMut(&mut Fe
// when no output has a pending frame. // when no output has a pending frame.
let wallpaper_changed = state.wallpaper.poll(); let wallpaper_changed = state.wallpaper.poll();
// All input/Wayland callbacks have returned, releasing seat locks. // All input/Wayland callbacks have returned, releasing seat locks.
if state.focus_cycle.is_some() && (state.session_lock.active() || state.input_capture.captures(1)) { if state.focus_cycle.is_some()
&& (state.session_lock.active() || state.input_capture.captures(1))
{
state.cancel_focus_cycle(); state.cancel_focus_cycle();
} }
+18 -5
View File
@@ -108,8 +108,14 @@ impl RenderMetrics {
.unwrap_or(0); .unwrap_or(0);
self.frame_times.make_contiguous().sort_unstable(); self.frame_times.make_contiguous().sort_unstable();
let percentile = |percent: usize| { let percentile = |percent: usize| {
let index = (self.frame_times.len() * percent).div_ceil(100).saturating_sub(1); let index = (self.frame_times.len() * percent)
self.frame_times.get(index).copied().unwrap_or_default().as_micros() .div_ceil(100)
.saturating_sub(1);
self.frame_times
.get(index)
.copied()
.unwrap_or_default()
.as_micros()
}; };
tracing::info!( tracing::info!(
target: "ferese::render", target: "ferese::render",
@@ -145,7 +151,8 @@ impl RenderMetrics {
} }
fn rectangle_area(rectangle: &Rectangle<i32, Physical>) -> u64 { 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)] #[cfg(test)]
@@ -240,8 +247,14 @@ mod tests {
for micros in 0..600 { for micros in 0..600 {
metrics.record_frame(Duration::from_micros(micros), &[], 0, FrameEffectMetrics); metrics.record_frame(Duration::from_micros(micros), &[], 0, FrameEffectMetrics);
} }
assert_eq!(metrics.frame_times.front(), Some(&Duration::from_micros(88))); assert_eq!(
assert_eq!(metrics.frame_times.back(), Some(&Duration::from_micros(599))); metrics.frame_times.front(),
Some(&Duration::from_micros(88))
);
assert_eq!(
metrics.frame_times.back(),
Some(&Duration::from_micros(599))
);
assert!( assert!(
metrics metrics
.frame_times .frame_times
+24 -7
View File
@@ -26,7 +26,10 @@ impl Identity {
pub fn from_edid(edid: &[u8]) -> Option<Self> { pub fn from_edid(edid: &[u8]) -> Option<Self> {
if edid.len() < 128 if edid.len() < 128
|| edid[..8] != [0, 255, 255, 255, 255, 255, 255, 0] || edid[..8] != [0, 255, 255, 255, 255, 255, 255, 0]
|| edid[..128].iter().fold(0u8, |sum, byte| sum.wrapping_add(*byte)) != 0 || edid[..128]
.iter()
.fold(0u8, |sum, byte| sum.wrapping_add(*byte))
!= 0
{ {
return None; return None;
} }
@@ -62,7 +65,11 @@ impl Identity {
if serial.is_none() && numeric_serial != 0 && numeric_serial != u32::MAX { if serial.is_none() && numeric_serial != 0 && numeric_serial != u32::MAX {
serial = Some(format!("{numeric_serial:08X}")); serial = Some(format!("{numeric_serial:08X}"));
} }
Some(Self { make, model, serial }) Some(Self {
make,
model,
serial,
})
} }
pub fn base(&self) -> String { pub fn base(&self) -> String {
@@ -90,8 +97,12 @@ impl IdentityRegistry {
*counts.entry(identity.base()).or_insert(0) += 1; *counts.entry(identity.base()).or_insert(0) += 1;
} }
} }
self.ambiguous self.ambiguous.extend(
.extend(counts.into_iter().filter(|(_, count)| *count > 1).map(|(base, _)| base)); counts
.into_iter()
.filter(|(_, count)| *count > 1)
.map(|(base, _)| base),
);
// Prefer already-known identities, but never permit duplicate runtime IDs. // Prefer already-known identities, but never permit duplicate runtime IDs.
let mut used = HashSet::new(); let mut used = HashSet::new();
let mut ordered = monitors.iter().enumerate().collect::<Vec<_>>(); let mut ordered = monitors.iter().enumerate().collect::<Vec<_>>();
@@ -107,12 +118,15 @@ impl IdentityRegistry {
.known .known
.get(key) .get(key)
.filter(|known| { .filter(|known| {
base.as_ref().is_some_and(|base| *known == base || *known == &qualified) base.as_ref()
.is_some_and(|base| *known == base || *known == &qualified)
|| (base.is_none() && *known == &qualified) || (base.is_none() && *known == &qualified)
}) })
.cloned() .cloned()
.unwrap_or_else(|| { .unwrap_or_else(|| {
if identity.as_ref().is_some_and(|identity| identity.serial.is_some()) if identity
.as_ref()
.is_some_and(|identity| identity.serial.is_some())
&& !self.ambiguous.contains(base.as_ref().unwrap()) && !self.ambiguous.contains(base.as_ref().unwrap())
{ {
base.clone().unwrap() base.clone().unwrap()
@@ -208,7 +222,10 @@ mod tests {
edid[10..12].copy_from_slice(&0x1234u16.to_le_bytes()); edid[10..12].copy_from_slice(&0x1234u16.to_le_bytes());
edid[12..16].copy_from_slice(&42u32.to_le_bytes()); edid[12..16].copy_from_slice(&42u32.to_le_bytes());
edid[127] = 0u8.wrapping_sub(edid.iter().fold(0u8, |sum, byte| sum.wrapping_add(*byte))); edid[127] = 0u8.wrapping_sub(edid.iter().fold(0u8, |sum, byte| sum.wrapping_add(*byte)));
assert_eq!(Identity::from_edid(&edid).unwrap().base(), "edid:DEL:1234:0000002A"); assert_eq!(
Identity::from_edid(&edid).unwrap().base(),
"edid:DEL:1234:0000002A"
);
edid[127] ^= 1; edid[127] ^= 1;
assert!(Identity::from_edid(&edid).is_none()); assert!(Identity::from_edid(&edid).is_none());
} }
+84 -25
View File
@@ -16,7 +16,9 @@ pub(crate) struct Monitor {
impl Monitor { impl Monitor {
pub fn matches(&self, selector: &str) -> bool { pub fn matches(&self, selector: &str) -> bool {
selector == self.connector || selector == self.identity || self.aliases.iter().any(|alias| alias == selector) selector == self.connector
|| selector == self.identity
|| self.aliases.iter().any(|alias| alias == selector)
} }
} }
@@ -59,9 +61,9 @@ impl ManualOverride {
self.layout = None; self.layout = None;
} }
if self.profile.as_ref().is_some_and(|name| { if self.profile.as_ref().is_some_and(|name| {
!profiles !profiles.iter().any(|profile| {
.iter() profile.name == *name && profile_matches(profile, monitors, lid_closed)
.any(|profile| profile.name == *name && profile_matches(profile, monitors, lid_closed)) })
}) { }) {
self.profile = None; self.profile = None;
} }
@@ -79,7 +81,9 @@ fn profile_matches(profile: &OutputProfile, monitors: &[Monitor], lid_closed: bo
}) { }) {
return false; return false;
} }
profile.lid_closed.is_none_or(|required| required == lid_closed) profile
.lid_closed
.is_none_or(|required| required == lid_closed)
&& profile && profile
.outputs .outputs
.iter() .iter()
@@ -119,9 +123,9 @@ pub(crate) fn select_profile(
.profile .profile
.as_ref() .as_ref()
.and_then(|name| { .and_then(|name| {
profiles profiles.iter().find(|profile| {
.iter() profile.name == *name && profile_matches(profile, monitors, lid_closed)
.find(|profile| profile.name == *name && profile_matches(profile, monitors, lid_closed)) })
}) })
.or_else(|| { .or_else(|| {
profiles profiles
@@ -130,7 +134,11 @@ pub(crate) fn select_profile(
.filter(|(_, profile)| profile_matches(profile, monitors, lid_closed)) .filter(|(_, profile)| profile_matches(profile, monitors, lid_closed))
.max_by_key(|(index, profile)| { .max_by_key(|(index, profile)| {
( (
profile.outputs.iter().filter(|output| output.required).count(), profile
.outputs
.iter()
.filter(|output| output.required)
.count(),
std::cmp::Reverse(*index), std::cmp::Reverse(*index),
) )
}) })
@@ -140,7 +148,9 @@ pub(crate) fn select_profile(
.layout .layout
.unwrap_or_else(|| selected.map_or(OutputLayout::Extend, |profile| profile.layout)); .unwrap_or_else(|| selected.map_or(OutputLayout::Extend, |profile| profile.layout));
let manage_lid = selected.is_none_or(|profile| profile.lid_policy == LidPolicy::DockOrSuspend); let manage_lid = selected.is_none_or(|profile| profile.lid_policy == LidPolicy::DockOrSuspend);
let external = monitors.iter().any(|monitor| monitor.usable && !monitor.internal); let external = monitors
.iter()
.any(|monitor| monitor.usable && !monitor.internal);
let mut outputs = monitors let mut outputs = monitors
.iter() .iter()
.map(|monitor| { .map(|monitor| {
@@ -229,7 +239,11 @@ pub(crate) fn restore_configuration(
.outputs .outputs
.retain(|output| monitors.iter().any(|monitor| monitor.key == output.key)); .retain(|output| monitors.iter().any(|monitor| monitor.key == output.key));
for monitor in monitors { for monitor in monitors {
if !restored.outputs.iter().any(|output| output.key == monitor.key) { if !restored
.outputs
.iter()
.any(|output| output.key == monitor.key)
{
let mut settings = defaults(monitor.identity.clone()); let mut settings = defaults(monitor.identity.clone());
settings.enabled = false; settings.enabled = false;
restored.outputs.push(DesiredOutput { restored.outputs.push(DesiredOutput {
@@ -254,7 +268,11 @@ pub(crate) fn restore_configuration(
output.mirror_source = None; output.mirror_source = None;
} }
} }
if !restored.outputs.iter().any(|output| output.settings.enabled) { if !restored
.outputs
.iter()
.any(|output| output.settings.enabled)
{
return select_profile(monitors, false, &[], &ManualOverride::default()); return select_profile(monitors, false, &[], &ManualOverride::default());
} }
restored restored
@@ -278,7 +296,8 @@ pub(crate) fn changed_outputs<'a>(
let next = new.outputs.iter().find(|candidate| candidate.key == key); let next = new.outputs.iter().find(|candidate| candidate.key == key);
let same = match (previous, next) { let same = match (previous, next) {
(Some(previous), Some(next)) => { (Some(previous), Some(next)) => {
previous.settings.same_configuration(&next.settings) && previous.mirror_source == next.mirror_source previous.settings.same_configuration(&next.settings)
&& previous.mirror_source == next.mirror_source
} }
(None, None) => true, (None, None) => true,
_ => false, _ => false,
@@ -294,7 +313,11 @@ pub(crate) struct Debouncer {
} }
impl Debouncer { impl Debouncer {
pub fn request(&mut self, now: std::time::Duration, active: bool) -> Option<std::time::Duration> { pub fn request(
&mut self,
now: std::time::Duration,
active: bool,
) -> Option<std::time::Duration> {
self.deadline = active.then(|| now + std::time::Duration::from_millis(150)); self.deadline = active.then(|| now + std::time::Duration::from_millis(150));
self.deadline self.deadline
} }
@@ -341,7 +364,9 @@ pub(crate) fn arrange_positions(
automatic.push((index, placement)); automatic.push((index, placement));
} }
} }
automatic.sort_by(|(_, a), (_, b)| (a.previous.is_none(), &a.key).cmp(&(b.previous.is_none(), &b.key))); automatic.sort_by(|(_, a), (_, b)| {
(a.previous.is_none(), &a.key).cmp(&(b.previous.is_none(), &b.key))
});
for (index, placement) in automatic { for (index, placement) in automatic {
let fits = |position: [i32; 2]| { let fits = |position: [i32; 2]| {
let (x, y, w, h) = ( let (x, y, w, h) = (
@@ -351,7 +376,10 @@ pub(crate) fn arrange_positions(
placement.size.1 as i64, placement.size.1 as i64,
); );
!occupied.iter().any(|&(ox, oy, ow, oh)| { !occupied.iter().any(|&(ox, oy, ow, oh)| {
x < ox as i64 + ow as i64 && x + w > ox as i64 && y < oy as i64 + oh as i64 && y + h > oy as i64 x < ox as i64 + ow as i64
&& x + w > ox as i64
&& y < oy as i64 + oh as i64
&& y + h > oy as i64
}) })
}; };
let previous = placement.previous.filter(|position| fits(*position)); let previous = placement.previous.filter(|position| fits(*position));
@@ -463,7 +491,10 @@ mod tests {
fn profile(name: &str, outputs: &[&str], layout: OutputLayout) -> OutputProfile { fn profile(name: &str, outputs: &[&str], layout: OutputLayout) -> OutputProfile {
OutputProfile { OutputProfile {
name: name.into(), name: name.into(),
outputs: outputs.iter().map(|name| defaults((*name).into())).collect(), outputs: outputs
.iter()
.map(|name| defaults((*name).into()))
.collect(),
layout, layout,
confirm_timeout: 15, confirm_timeout: 15,
lid_policy: LidPolicy::DockOrSuspend, lid_policy: LidPolicy::DockOrSuspend,
@@ -559,7 +590,11 @@ mod tests {
&ManualOverride::default(), &ManualOverride::default(),
); );
assert_eq!( assert_eq!(
desired.outputs.iter().filter(|output| output.settings.enabled).count(), desired
.outputs
.iter()
.filter(|output| output.settings.enabled)
.count(),
count count
); );
assert_eq!( assert_eq!(
@@ -574,7 +609,12 @@ mod tests {
let missing = [profile("absent", &["DP-9"], OutputLayout::ExternalOnly)]; let missing = [profile("absent", &["DP-9"], OutputLayout::ExternalOnly)];
let fallback = select_profile(&monitors, false, &missing, &ManualOverride::default()); let fallback = select_profile(&monitors, false, &missing, &ManualOverride::default());
assert_eq!(fallback.profile, None); assert_eq!(fallback.profile, None);
assert!(fallback.outputs.iter().all(|output| output.settings.enabled)); assert!(
fallback
.outputs
.iter()
.all(|output| output.settings.enabled)
);
let internal = &monitors[..1]; let internal = &monitors[..1];
let mut disabled = profile("disabled", &["eDP-1"], OutputLayout::Extend); let mut disabled = profile("disabled", &["eDP-1"], OutputLayout::Extend);
disabled.outputs[0].enabled = false; disabled.outputs[0].enabled = false;
@@ -631,13 +671,23 @@ mod tests {
let monitors = [monitor("eDP-1", true), monitor("DP-1", false)]; let monitors = [monitor("eDP-1", true), monitor("DP-1", false)];
let mut mirror = profile("mirror", &["eDP-1", "DP-1"], OutputLayout::Mirror); let mut mirror = profile("mirror", &["eDP-1", "DP-1"], OutputLayout::Mirror);
mirror.mirror_source = Some("eDP-1".into()); mirror.mirror_source = Some("eDP-1".into());
let old = select_profile(&monitors, false, &[mirror.clone()], &ManualOverride::default()); let old = select_profile(
&monitors,
false,
&[mirror.clone()],
&ManualOverride::default(),
);
assert_eq!(old.outputs[1].mirror_source.as_deref(), Some("eDP-1")); assert_eq!(old.outputs[1].mirror_source.as_deref(), Some("eDP-1"));
let removed = select_profile(&monitors[..1], false, &[mirror], &ManualOverride::default()); let removed = select_profile(&monitors[..1], false, &[mirror], &ManualOverride::default());
assert!(removed.outputs[0].mirror_source.is_none()); assert!(removed.outputs[0].mirror_source.is_none());
assert!(removed.outputs[0].settings.enabled); assert!(removed.outputs[0].settings.enabled);
let extended = select_profile(&monitors, false, &[], &ManualOverride::default()); let extended = select_profile(&monitors, false, &[], &ManualOverride::default());
assert!(extended.outputs.iter().all(|output| output.mirror_source.is_none())); assert!(
extended
.outputs
.iter()
.all(|output| output.mirror_source.is_none())
);
assert_eq!(changed_outputs(&old, &extended), vec!["DP-1"]); assert_eq!(changed_outputs(&old, &extended), vec!["DP-1"]);
} }
@@ -648,9 +698,14 @@ mod tests {
mobile.outputs[1].required = false; mobile.outputs[1].required = false;
mobile.lid_closed = Some(false); mobile.lid_closed = Some(false);
assert_eq!( assert_eq!(
select_profile(&monitors, false, &[mobile.clone()], &ManualOverride::default()) select_profile(
.profile &monitors,
.as_deref(), false,
&[mobile.clone()],
&ManualOverride::default()
)
.profile
.as_deref(),
Some("mobile") Some("mobile")
); );
assert_eq!( assert_eq!(
@@ -679,7 +734,11 @@ mod tests {
assert_eq!(restored.outputs.len(), 1); assert_eq!(restored.outputs.len(), 1);
assert!(restored.outputs[0].settings.enabled); assert!(restored.outputs[0].settings.enabled);
let unknown = [monitor("HDMI-A-2", false)]; let unknown = [monitor("HDMI-A-2", false)];
assert!(restore_configuration(&previous, &unknown).outputs[0].settings.enabled); assert!(
restore_configuration(&previous, &unknown).outputs[0]
.settings
.enabled
);
} }
} }
+14 -3
View File
@@ -152,7 +152,12 @@ mod tests {
let inventory = |active: &BTreeSet<u8>| { let inventory = |active: &BTreeSet<u8>| {
active active
.iter() .iter()
.map(|id| (OutputId(*id as u64), OutputGeometry::new(*id as i32 * 800, 0, 800, 600))) .map(|id| {
(
OutputId(*id as u64),
OutputGeometry::new(*id as i32 * 800, 0, 800, 600),
)
})
.collect::<Vec<_>>() .collect::<Vec<_>>()
}; };
for rollback_fails in [false, true] { for rollback_fails in [false, true] {
@@ -184,8 +189,14 @@ mod tests {
) )
.unwrap(); .unwrap();
if rollback_fails { if rollback_fails {
assert_eq!(plan.outputs.connected_outputs().collect::<Vec<_>>(), vec![OutputId(2)]); assert_eq!(
assert_eq!(plan.outputs.output_for_workspace(WorkspaceId(1)), Some(OutputId(2))); plan.outputs.connected_outputs().collect::<Vec<_>>(),
vec![OutputId(2)]
);
assert_eq!(
plan.outputs.output_for_workspace(WorkspaceId(1)),
Some(OutputId(2))
);
} else { } else {
assert_eq!(plan.outputs, published); assert_eq!(plan.outputs, published);
assert!(plan.affected_outputs.is_empty()); assert!(plan.affected_outputs.is_empty());
+253 -92
View File
@@ -41,14 +41,17 @@ pub(crate) fn init_font_loader(
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<(), Box<dyn std::error::Error>> {
use smithay::reexports::calloop::channel; use smithay::reexports::calloop::channel;
let (results, source) = channel::sync_channel::<(String, Option<FontArc>)>(1); let (results, source) = channel::sync_channel::<(String, Option<FontArc>)>(1);
event_loop.handle().insert_source(source, |event, _, state| { event_loop
if let channel::Event::Msg((family, font)) = event .handle()
&& state.overview.complete_font_load(&family, font) .insert_source(source, |event, _, state| {
&& (state.overview.is_presenting() || state.tab_bars.values().any(|bars| !bars.is_empty())) if let channel::Event::Msg((family, font)) = event
{ && state.overview.complete_font_load(&family, font)
crate::backends::direct::render_all(state); && (state.overview.is_presenting()
} || state.tab_bars.values().any(|bars| !bars.is_empty()))
})?; {
crate::backends::direct::render_all(state);
}
})?;
let latest = Arc::new(Mutex::new(state.overview.font_family.clone())); let latest = Arc::new(Mutex::new(state.overview.font_family.clone()));
let worker_latest = latest.clone(); let worker_latest = latest.clone();
let (wake, requests) = mpsc::sync_channel(1); let (wake, requests) = mpsc::sync_channel(1);
@@ -98,7 +101,10 @@ fn label_font(database: &fontdb::Database, family: &str) -> Option<FontArc> {
}) })
} }
type LabelBuffer = (MemoryRenderBuffer, smithay::utils::Size<i32, smithay::utils::Buffer>); type LabelBuffer = (
MemoryRenderBuffer,
smithay::utils::Size<i32, smithay::utils::Buffer>,
);
type LabelStyle = (u64, u64, [u32; 4]); type LabelStyle = (u64, u64, [u32; 4]);
type OverviewStateLabel = HashMap<String, HashMap<LabelStyle, LabelBuffer>>; type OverviewStateLabel = HashMap<String, HashMap<LabelStyle, LabelBuffer>>;
@@ -170,11 +176,19 @@ pub(crate) fn workspace_strip(bounds: Rect) -> Rect {
fn window_area(bounds: Rect) -> Rect { fn window_area(bounds: Rect) -> Rect {
let strip = workspace_strip(bounds); let strip = workspace_strip(bounds);
let top = strip.y + strip.height + 16.0; 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 { 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 { fn strip_capacity(strip: Rect) -> usize {
@@ -192,11 +206,10 @@ impl OverviewState {
pub(crate) fn needs_tick(&self) -> bool { pub(crate) fn needs_tick(&self) -> bool {
self.motion.opacity.current != self.motion.opacity.target self.motion.opacity.current != self.motion.opacity.target
|| self.motion.opacity.velocity != 0.0 || self.motion.opacity.velocity != 0.0
|| self || self.motion.presentations.values().any(|rect| {
.motion rect.current != rect.target
.presentations || rect.velocity != ferese_animation::RectVelocity::default()
.values() })
.any(|rect| rect.current != rect.target || rect.velocity != ferese_animation::RectVelocity::default())
} }
pub(crate) fn is_animating(&self, spring: SpringConfig) -> bool { pub(crate) fn is_animating(&self, spring: SpringConfig) -> bool {
@@ -246,12 +259,25 @@ impl OverviewState {
text: &str, text: &str,
scale: f64, scale: f64,
color: [f32; 4], color: [f32; 4],
) -> Option<(MemoryRenderBuffer, smithay::utils::Size<i32, smithay::utils::Buffer>)> { ) -> Option<(
MemoryRenderBuffer,
smithay::utils::Size<i32, smithay::utils::Buffer>,
)> {
self.label_with_size(text, scale, color, 13.0) self.label_with_size(text, scale, color, 13.0)
} }
fn label_with_size(&mut self, text: &str, scale: f64, color: [f32; 4], font_size: f64) -> Option<LabelBuffer> { fn label_with_size(
let key = (scale.to_bits(), font_size.to_bits(), color.map(f32::to_bits)); &mut self,
text: &str,
scale: f64,
color: [f32; 4],
font_size: f64,
) -> Option<LabelBuffer> {
let key = (
scale.to_bits(),
font_size.to_bits(),
color.map(f32::to_bits),
);
if let Some(buffer) = self.labels.get(text).and_then(|styles| styles.get(&key)) { if let Some(buffer) = self.labels.get(text).and_then(|styles| styles.get(&key)) {
return Some(buffer.clone()); return Some(buffer.clone());
} }
@@ -269,7 +295,10 @@ impl OverviewState {
let mut pen = 2.0; let mut pen = 2.0;
for ch in text.chars() { for ch in text.chars() {
let id = font.glyph_id(ch); 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); pen += scaled.h_advance(id);
if let Some(outline) = font.outline_glyph(glyph) { if let Some(outline) = font.outline_glyph(glyph) {
let bounds = outline.px_bounds(); let bounds = outline.px_bounds();
@@ -289,8 +318,14 @@ impl OverviewState {
}); });
} }
} }
let buffer = let buffer = MemoryRenderBuffer::from_slice(
MemoryRenderBuffer::from_slice(&pixels, Fourcc::Abgr8888, (width, height), 1, Transform::Normal, None); &pixels,
Fourcc::Abgr8888,
(width, height),
1,
Transform::Normal,
None,
);
if self.label_count >= 128 { if self.label_count >= 128 {
self.labels.clear(); self.labels.clear();
self.label_count = 0; self.label_count = 0;
@@ -318,12 +353,19 @@ impl OverviewState {
} }
fn select_workspace_windows(&mut self, windows: &[WindowId], preferred: Option<WindowId>) { fn select_workspace_windows(&mut self, windows: &[WindowId], preferred: Option<WindowId>) {
let visible = |id: &WindowId| windows.contains(id) && self.motion.presentations.contains_key(id); let visible =
|id: &WindowId| windows.contains(id) && self.motion.presentations.contains_key(id);
self.selected = self self.selected = self
.selected .selected
.filter(visible) .filter(visible)
.or_else(|| preferred.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 selection_state(&self) -> (Option<WindowId>, Option<WorkspaceId>) { pub(crate) fn selection_state(&self) -> (Option<WindowId>, Option<WorkspaceId>) {
@@ -363,9 +405,13 @@ impl OverviewState {
.filter(|(id, _)| Some(**id) != self.selected) .filter(|(id, _)| Some(**id) != self.selected)
.filter_map(|(id, presentation)| { .filter_map(|(id, presentation)| {
let center = rect_center(presentation.current); let center = rect_center(presentation.current);
directional_distance(direction, current_center, center).map(|distance| (*id, distance)) 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))
}) })
.min_by(|(left_id, left), (right_id, right)| left.total_cmp(right).then_with(|| left_id.0.cmp(&right_id.0)))
.map(|(id, _)| id); .map(|(id, _)| id);
let Some(next) = next else { let Some(next) = next else {
return false; return false;
@@ -385,26 +431,33 @@ impl OverviewState {
&self.motion &self.motion
} }
fn seed_presentations(&mut self, windows: impl IntoIterator<Item = crate::presentation::WindowPresentation>) { fn seed_presentations(
&mut self,
windows: impl IntoIterator<Item = crate::presentation::WindowPresentation>,
) {
for window in windows { for window in windows {
self.motion.presentations.entry(window.id).or_insert_with(|| { self.motion
// Opening scales the final presentation after overview. Remove .presentations
// it here so the same opening motion is not applied twice. .entry(window.id)
let scale = 0.97 + 0.03 * window.opacity.current; .or_insert_with(|| {
let mut bounds = window.bounds; // Opening scales the final presentation after overview. Remove
bounds.velocity = crate::presentation::scaled_visual_velocity( // it here so the same opening motion is not applied twice.
bounds.current, let scale = 0.97 + 0.03 * window.opacity.current;
bounds.velocity, let mut bounds = window.bounds;
1.0 / scale, bounds.velocity = crate::presentation::scaled_visual_velocity(
-0.03 * window.opacity.velocity / (scale * scale), bounds.current,
); bounds.velocity,
bounds.current = crate::presentation::scaled_visual_rect(bounds.current, 1.0 / scale); 1.0 / scale,
bounds.velocity.x /= OVERVIEW_MOTION_SPEED; -0.03 * window.opacity.velocity / (scale * scale),
bounds.velocity.y /= OVERVIEW_MOTION_SPEED; );
bounds.velocity.width /= OVERVIEW_MOTION_SPEED; bounds.current =
bounds.velocity.height /= OVERVIEW_MOTION_SPEED; crate::presentation::scaled_visual_rect(bounds.current, 1.0 / scale);
bounds bounds.velocity.x /= OVERVIEW_MOTION_SPEED;
}); bounds.velocity.y /= OVERVIEW_MOTION_SPEED;
bounds.velocity.width /= OVERVIEW_MOTION_SPEED;
bounds.velocity.height /= OVERVIEW_MOTION_SPEED;
bounds
});
} }
} }
@@ -428,7 +481,9 @@ impl OverviewState {
} }
fn retarget(&mut self, targets: HashMap<WindowId, (Rect, Rect)>, animations_enabled: bool) { fn retarget(&mut self, targets: HashMap<WindowId, (Rect, Rect)>, animations_enabled: bool) {
self.motion.presentations.retain(|id, _| targets.contains_key(id)); self.motion
.presentations
.retain(|id, _| targets.contains_key(id));
for (id, (normal, target)) in targets { for (id, (normal, target)) in targets {
let presentation = self let presentation = self
@@ -446,7 +501,12 @@ impl OverviewState {
.selected .selected
.is_none_or(|id| !self.motion.presentations.contains_key(&id)) .is_none_or(|id| !self.motion.presentations.contains_key(&id))
{ {
self.selected = self.motion.presentations.keys().copied().min_by_key(|id| id.0); self.selected = self
.motion
.presentations
.keys()
.copied()
.min_by_key(|id| id.0);
} }
} }
@@ -474,7 +534,12 @@ 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 {
self.motion.advance(delta, spring, animations_enabled) self.motion.advance(delta, spring, animations_enabled)
} }
} }
@@ -496,7 +561,10 @@ impl Ferese {
} }
} }
pub(crate) fn redraw_overview_selection(&mut self, previous: (Option<WindowId>, Option<WorkspaceId>)) { pub(crate) fn redraw_overview_selection(
&mut self,
previous: (Option<WindowId>, Option<WorkspaceId>),
) {
let outputs = self let outputs = self
.space .space
.outputs() .outputs()
@@ -509,7 +577,8 @@ impl Ferese {
.into_iter() .into_iter()
.flatten() .flatten()
.any(|workspace| { .any(|workspace| {
self.output_workspaces.output_for_workspace(workspace) == self.output_id(output) self.output_workspaces.output_for_workspace(workspace)
== self.output_id(output)
}) })
}) })
.cloned() .cloned()
@@ -523,7 +592,12 @@ impl Ferese {
.retain(|id, _| self.windows.window(*id).is_some()); .retain(|id, _| self.windows.window(*id).is_some());
} }
pub(crate) fn overview_window_label(&mut self, id: WindowId, scale: f64, width: f64) -> Option<LabelBuffer> { pub(crate) fn overview_window_label(
&mut self,
id: WindowId,
scale: f64,
width: f64,
) -> Option<LabelBuffer> {
self.window_title_label(id, scale, width, 13.0) self.window_title_label(id, scale, width, 13.0)
} }
@@ -537,7 +611,11 @@ impl Ferese {
let record = self.windows.record(id)?; let record = self.windows.record(id)?;
let limit = ((width - 16.0) / 8.0).clamp(1.0, 48.0) as usize; let limit = ((width - 16.0) / 8.0).clamp(1.0, 48.0) as usize;
let color = self.theme_settings.text_primary_color.0; let color = self.theme_settings.text_primary_color.0;
let style = (scale.to_bits(), font_size.to_bits(), color.map(f32::to_bits)); let style = (
scale.to_bits(),
font_size.to_bits(),
color.map(f32::to_bits),
);
if let Some(label) = self.overview.window_labels.get(&id) if let Some(label) = self.overview.window_labels.get(&id)
&& label.revision == record.metadata_revision && label.revision == record.metadata_revision
@@ -563,7 +641,9 @@ impl Ferese {
} }
let revision = record.metadata_revision; let revision = record.metadata_revision;
let buffer = self.overview.label_with_size(&text, scale, color, font_size)?; let buffer = self
.overview
.label_with_size(&text, scale, color, font_size)?;
self.overview.window_labels.insert( self.overview.window_labels.insert(
id, id,
WindowLabel { WindowLabel {
@@ -604,9 +684,15 @@ impl Ferese {
&mut self, &mut self,
index: u32, index: u32,
scale: f64, scale: f64,
) -> Option<(MemoryRenderBuffer, smithay::utils::Size<i32, smithay::utils::Buffer>)> { ) -> Option<(
self.overview MemoryRenderBuffer,
.label(&index.to_string(), scale, self.theme_settings.text_primary_color.0) smithay::utils::Size<i32, smithay::utils::Buffer>,
)> {
self.overview.label(
&index.to_string(),
scale,
self.theme_settings.text_primary_color.0,
)
} }
pub(crate) fn set_overview_active(&mut self, active: bool) { pub(crate) fn set_overview_active(&mut self, active: bool) {
@@ -676,14 +762,17 @@ impl Ferese {
if !self.overview.is_active() { if !self.overview.is_active() {
if self.overview.is_presenting() { if self.overview.is_presenting() {
let normal = self.normal_window_rects(); let normal = self.normal_window_rects();
self.overview.motion.presentations.retain(|id, presentation| { self.overview
if let Some(target) = normal.get(id) { .motion
presentation.set_target(*target); .presentations
true .retain(|id, presentation| {
} else { if let Some(target) = normal.get(id) {
false presentation.set_target(*target);
} true
}); } else {
false
}
});
} }
return; return;
} }
@@ -691,7 +780,13 @@ impl Ferese {
let targets = self.overview_targets(); let targets = self.overview_targets();
self.overview.retarget(targets, self.animations_enabled()); self.overview.retarget(targets, self.animations_enabled());
if let Some(cycle) = self.focus_cycle.as_mut() { if let Some(cycle) = self.focus_cycle.as_mut() {
let available = self.overview.motion.presentations.keys().copied().collect::<Vec<_>>(); let available = self
.overview
.motion
.presentations
.keys()
.copied()
.collect::<Vec<_>>();
if let Some(id) = cycle.reconcile(&available) { if let Some(id) = cycle.reconcile(&available) {
self.overview.select_window(id); self.overview.select_window(id);
} }
@@ -711,7 +806,8 @@ impl Ferese {
.collect::<Vec<_>>() .collect::<Vec<_>>()
}) })
.unwrap_or_default(); .unwrap_or_default();
self.overview.select_workspace_windows(&windows, self.focused_window); self.overview
.select_workspace_windows(&windows, self.focused_window);
} }
} }
@@ -719,7 +815,12 @@ impl Ferese {
Some(self.current_window_presentation(id)?.bounds.current) Some(self.current_window_presentation(id)?.bounds.current)
} }
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.windows.geometry(&id)?; let geometry = self.windows.geometry(&id)?;
if !self.overview.is_presenting() && self.windows.record(id)?.opening.is_none() { if !self.overview.is_presenting() && self.windows.record(id)?.opening.is_none() {
let presented = self.presented_window_rect(id)?; let presented = self.presented_window_rect(id)?;
@@ -732,7 +833,11 @@ impl Ferese {
}; };
let presented = self.presented_window_rect(id)?; let presented = self.presented_window_rect(id)?;
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; return None;
} }
@@ -780,9 +885,10 @@ impl Ferese {
.copied() .copied()
.unwrap_or_else(|| selected_index.saturating_sub(capacity - 1)) .unwrap_or_else(|| selected_index.saturating_sub(capacity - 1))
.min(max_offset); .min(max_offset);
let card_width = ((strip.width - 24.0 - WORKSPACE_CARD_GAP * capacity.saturating_sub(1) as f64) let card_width =
/ capacity as f64) ((strip.width - 24.0 - WORKSPACE_CARD_GAP * capacity.saturating_sub(1) as f64)
.clamp(1.0, 160.0); / capacity as f64)
.clamp(1.0, 160.0);
let visible_count = workspaces.len().saturating_sub(offset).min(capacity); let visible_count = workspaces.len().saturating_sub(offset).min(capacity);
let start_x = centered_row_start(strip, visible_count, card_width); let start_x = centered_row_start(strip, visible_count, card_width);
workspaces workspaces
@@ -791,7 +897,10 @@ impl Ferese {
.take(capacity) .take(capacity)
.enumerate() .enumerate()
.map(|(index, view)| { .map(|(index, view)| {
let workspace = self.workspaces.workspace(view.id).expect("workspace view exists"); let workspace = self
.workspaces
.workspace(view.id)
.expect("workspace view exists");
let rect = Rect::new( let rect = Rect::new(
start_x + index as f64 * (card_width + WORKSPACE_CARD_GAP), start_x + index as f64 * (card_width + WORKSPACE_CARD_GAP),
@@ -861,8 +970,11 @@ impl Ferese {
self.advance_animations(std::time::Instant::now()); self.advance_animations(std::time::Instant::now());
let previous = self.output_workspaces.active_workspace(output_id); let previous = self.output_workspaces.active_workspace(output_id);
if let Ok( if let Ok(
ferese_core::WorkspaceSwitch::Activated(owner) | ferese_core::WorkspaceSwitch::FocusedExisting(owner), ferese_core::WorkspaceSwitch::Activated(owner)
) = self.output_workspaces.switch_workspace(output_id, card.workspace) | ferese_core::WorkspaceSwitch::FocusedExisting(owner),
) = self
.output_workspaces
.switch_workspace(output_id, card.workspace)
{ {
self.update_workspace_slide(output_id, owner, previous, card.workspace, None); self.update_workspace_slide(output_id, owner, previous, card.workspace, None);
// Existing monitor ownership wins; never steal another output's workspace. // Existing monitor ownership wins; never steal another output's workspace.
@@ -889,7 +1001,9 @@ impl Ferese {
}; };
let capacity = strip_capacity(workspace_strip(bounds)); let capacity = strip_capacity(workspace_strip(bounds));
let cards = self.overview_workspace_cards(&output); let cards = self.overview_workspace_cards(&output);
let ordered = self.output_workspaces.ordered_workspaces(&self.workspaces, output_id); let ordered = self
.output_workspaces
.ordered_workspaces(&self.workspaces, output_id);
let current = cards let current = cards
.first() .first()
.and_then(|card| ordered.iter().position(|id| *id == card.workspace)) .and_then(|card| ordered.iter().position(|id| *id == card.workspace))
@@ -933,12 +1047,17 @@ impl Ferese {
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
for (id, target) in overview_layout(window_area(bounds), &windows) { for (id, target) in overview_layout(window_area(bounds), &windows) {
targets.insert(id, (self.windows.geometry(&id).unwrap().visual.current, target)); targets.insert(
id,
(self.windows.geometry(&id).unwrap().visual.current, target),
);
} }
continue; continue;
} }
let mut visible = vec![workspace]; let mut visible = vec![workspace];
let sliding = self.workspace_slide_workspaces(output_id).collect::<Vec<_>>(); let sliding = self
.workspace_slide_workspaces(output_id)
.collect::<Vec<_>>();
visible.extend(sliding.iter().copied().filter(|id| *id != workspace)); visible.extend(sliding.iter().copied().filter(|id| *id != workspace));
for workspace in visible { for workspace in visible {
let Some(workspace) = self.workspaces.workspace(workspace) else { let Some(workspace) = self.workspaces.workspace(workspace) else {
@@ -983,7 +1102,12 @@ fn overview_layout(bounds: Rect, windows: &[(WindowId, Rect)]) -> HashMap<Window
// Preserve input order for rendering: the damage tracker includes each // Preserve input order for rendering: the damage tracker includes each
// instance's z index, even when neighboring previews do not overlap. // instance's z index, even when neighboring previews do not overlap.
fn preview_layout(bounds: Rect, windows: &[(WindowId, Rect)], margin: f64, gap: f64) -> Vec<(WindowId, Rect)> { fn preview_layout(
bounds: Rect,
windows: &[(WindowId, Rect)],
margin: f64,
gap: f64,
) -> Vec<(WindowId, Rect)> {
if windows.is_empty() { if windows.is_empty() {
return Vec::new(); return Vec::new();
} }
@@ -996,8 +1120,10 @@ fn preview_layout(bounds: Rect, windows: &[(WindowId, Rect)], margin: f64, gap:
let cell_size = |columns: usize| { let cell_size = |columns: usize| {
let rows = windows.len().div_ceil(columns); 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_width - gap * columns.saturating_sub(1) as f64) / columns as f64)
((content_height - gap * rows.saturating_sub(1) as f64) / rows as f64).clamp(1.0, 420.0), .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| { let score = |columns: usize| {
@@ -1039,8 +1165,10 @@ fn preview_layout(bounds: Rect, windows: &[(WindowId, Rect)], margin: f64, gap:
let width = (source.width * scale).max(1.0); let width = (source.width * scale).max(1.0);
let height = (source.height * scale).max(1.0); let height = (source.height * scale).max(1.0);
let row_count = (windows.len() - row * columns).min(columns); 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 row_width =
let cell_x = bounds.x + (bounds.width - row_width) * 0.5 + column as f64 * (cell_width + gap); 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 cell_y = origin_y + row as f64 * (cell_height + gap);
let target = Rect::new( let target = Rect::new(
cell_x + (cell_width - width) / 2.0, cell_x + (cell_width - width) / 2.0,
@@ -1058,7 +1186,11 @@ fn rect_center(rect: Rect) -> (f64, f64) {
(rect.x + rect.width / 2.0, rect.y + rect.height / 2.0) (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_x = candidate.0 - current.0;
let delta_y = candidate.1 - current.1; let delta_y = candidate.1 - current.1;
let (primary, secondary) = match direction { let (primary, secondary) = match direction {
@@ -1086,7 +1218,10 @@ impl OverviewMotion {
pub(crate) fn is_animating(&self, spring: SpringConfig) -> bool { pub(crate) fn is_animating(&self, spring: SpringConfig) -> bool {
(self.opacity.current - self.opacity.target).abs() > 0.001 (self.opacity.current - self.opacity.target).abs() > 0.001
|| self.opacity.velocity.abs() > 0.005 || self.opacity.velocity.abs() > 0.005
|| self.presentations.values().any(|rect| !rect.is_settled(spring)) || self
.presentations
.values()
.any(|rect| !rect.is_settled(spring))
} }
pub(crate) fn is_presenting(&self) -> bool { pub(crate) fn is_presenting(&self) -> bool {
@@ -1113,7 +1248,12 @@ impl OverviewMotion {
self.presentations.get(&id).map_or(normal, |p| p.current) self.presentations.get(&id).map_or(normal, |p| p.current)
} }
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 {
let delta = delta.mul_f64(OVERVIEW_MOTION_SPEED); let delta = delta.mul_f64(OVERVIEW_MOTION_SPEED);
let mut active_animation = false; let mut active_animation = false;
@@ -1194,7 +1334,9 @@ mod tests {
Some(id), Some(id),
true, true,
); );
let sampled = overview.motion.presented_bounds(id, AnimatedRect::new(normal)); let sampled = overview
.motion
.presented_bounds(id, AnimatedRect::new(normal));
assert!((sampled.current.x - normal.x).abs() < 1e-10); assert!((sampled.current.x - normal.x).abs() < 1e-10);
assert!((sampled.current.width - normal.width).abs() < 1e-10); assert!((sampled.current.width - normal.width).abs() < 1e-10);
assert!((sampled.velocity.x - velocity.x).abs() < 1e-10); assert!((sampled.velocity.x - velocity.x).abs() < 1e-10);
@@ -1265,7 +1407,10 @@ mod tests {
requests.request(family); requests.request(family);
} }
receiver.try_recv().unwrap(); 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"); assert_eq!(*requests.latest.lock().unwrap(), "third");
drop(requests); drop(requests);
assert!(matches!(receiver.recv(), Err(mpsc::RecvError))); assert!(matches!(receiver.recv(), Err(mpsc::RecvError)));
@@ -1285,7 +1430,9 @@ mod tests {
let strip = Rect::new(origin + 24.0, 64.0, 1552.0, 132.0); let strip = Rect::new(origin + 24.0, 64.0, 1552.0, 132.0);
for count in [1, 3, 8] { for count in [1, 3, 8] {
let start = centered_row_start(strip, count, 160.0); 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); assert!((start - strip.x - (strip.x + strip.width - end)).abs() < 0.001);
} }
} }
@@ -1302,7 +1449,11 @@ mod tests {
assert!(strip.height < bounds.height / 2.0); assert!(strip.height < bounds.height / 2.0);
assert!(area.y > strip.y + strip.height); assert!(area.y > strip.y + strip.height);
let windows = [(WindowId(1), Rect::new(0.0, 0.0, 800.0, 900.0))]; 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)
);
} }
} }
@@ -1386,7 +1537,11 @@ mod tests {
let mut reference = AnimatedRect::new(normal); let mut reference = AnimatedRect::new(normal);
reference.set_target(thumbnail); reference.set_target(thumbnail);
reference.advance_with_policy(Duration::from_millis(60), spring, CrossingPolicy::NoCrossing); reference.advance_with_policy(
Duration::from_millis(60),
spring,
CrossingPolicy::NoCrossing,
);
assert_eq!(predicted.presented_rect(id, normal), reference.current); assert_eq!(predicted.presented_rect(id, normal), reference.current);
overview.advance(Duration::from_millis(100), spring, true); overview.advance(Duration::from_millis(100), spring, true);
@@ -1467,7 +1622,10 @@ mod tests {
#[test] #[test]
fn overview_grid_preserves_preview_aspect_ratio() { fn overview_grid_preserves_preview_aspect_ratio() {
let source = Rect::new(0.0, 0.0, 1200.0, 800.0); 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)]; let preview = layout[&WindowId(1)];
assert!((preview.width / preview.height - source.width / source.height).abs() < 0.001); assert!((preview.width / preview.height - source.width / source.height).abs() < 0.001);
@@ -1579,7 +1737,10 @@ mod tests {
let mut overview = OverviewState::default(); let mut overview = OverviewState::default();
overview.enter( 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), Some(left),
false, false,
); );
+80 -25
View File
@@ -103,21 +103,31 @@ impl PortalSession {
pub(crate) fn remove(&mut self, owner: u64) { pub(crate) fn remove(&mut self, owner: u64) {
self.waiters.remove(&owner); 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); self.set_phase(1);
} }
if self.inhibitors.remove(&owner).is_some() { if self.inhibitors.remove(&owner).is_some() {
self.inhibitor_revision = self.inhibitor_revision.wrapping_add(1); self.inhibitor_revision = self.inhibitor_revision.wrapping_add(1);
} }
let monitor_removed = self.monitors.remove(&owner); 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 { if monitor_removed || acknowledged {
self.changed(); self.changed();
} }
} }
pub(crate) fn idle_inhibited(&self) -> bool { pub(crate) 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 { pub(crate) fn begin_query(&mut self) -> u32 {
@@ -141,7 +151,11 @@ impl PortalSession {
} }
pub(crate) fn cancel_query(&mut self, token: u32) { 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); self.set_phase(1);
} }
} }
@@ -154,19 +168,37 @@ impl PortalSession {
.is_some_and(|query| query.pending.is_empty() || Instant::now() >= query.deadline) .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> { pub(crate) fn validate_end(
&self,
token: u32,
inhibitor_revision: u32,
force: bool,
) -> Result<(), String> {
if !self.query_ready() || self.query.as_ref().is_none_or(|query| query.token != token) { if !self.query_ready() || self.query.as_ref().is_none_or(|query| query.token != token) {
return Err("Session-ending confirmation expired or applications are still responding".into()); return Err(
"Session-ending confirmation expired or applications are still responding".into(),
);
} }
if self.inhibitor_revision != inhibitor_revision 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(()) 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.validate_end(token, inhibitor_revision, force)?;
self.set_phase(3); self.set_phase(3);
Ok(()) Ok(())
@@ -209,7 +241,13 @@ 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 { if since != self.revision {
let _ = response.try_send(Response::success(id, self.snapshot())); let _ = response.try_send(Response::success(id, self.snapshot()));
} else { } else {
@@ -224,7 +262,10 @@ impl PortalSession {
.map(|inhibitor| json!({"flags": inhibitor.flags, "app": inhibitor.app, "reason": inhibitor.reason})) .map(|inhibitor| json!({"flags": inhibitor.flags, "app": inhibitor.app, "reason": inhibitor.reason}))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let remaining = self.query.as_ref().map_or(0, |query| { 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, json!({"screensaver-active": self.locked, "session-state": self.phase, "revision": self.revision,
"inhibitors": inhibitors, "inhibitor-revision": self.inhibitor_revision, "inhibitors": inhibitors, "inhibitor-revision": self.inhibitor_revision,
@@ -236,16 +277,17 @@ impl PortalSession {
impl crate::Ferese { impl crate::Ferese {
pub(crate) fn end_portal_session(&mut self) { pub(crate) fn end_portal_session(&mut self) {
let token = self.portal_session.query.as_ref().map(|query| query.token); let token = self.portal_session.query.as_ref().map(|query| query.token);
let result = let result = self.loop_handle.insert_source(
self.loop_handle Timer::from_duration(Duration::from_millis(200)),
.insert_source(Timer::from_duration(Duration::from_millis(200)), move |_, _, state| { move |_, _, state| {
if state.portal_session.phase == 3 if state.portal_session.phase == 3
&& state.portal_session.query.as_ref().map(|query| query.token) == token && state.portal_session.query.as_ref().map(|query| query.token) == token
{ {
state.loop_signal.stop(); state.loop_signal.stop();
} }
TimeoutAction::Drop TimeoutAction::Drop
}); },
);
if let Err(error) = result { if let Err(error) = result {
tracing::error!(%error, "failed to schedule session ending"); tracing::error!(%error, "failed to schedule session ending");
self.loop_signal.stop(); self.loop_signal.stop();
@@ -304,10 +346,20 @@ mod tests {
let approved = session.inhibitor_revision; let approved = session.inhibitor_revision;
session.register(1, inhibition(1)).unwrap(); session.register(1, inhibition(1)).unwrap();
assert!(session.commit_end(token, approved, true).is_err()); assert!(session.commit_end(token, approved, true).is_err());
assert!(session.commit_end(token, session.inhibitor_revision, false).is_err()); assert!(
session.commit_end(token, session.inhibitor_revision, true).unwrap(); 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_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] #[test]
@@ -350,7 +402,10 @@ mod tests {
let response = receiver.try_recv().unwrap(); let response = receiver.try_recv().unwrap();
assert_eq!(response.result.unwrap()["screensaver-active"], true); assert_eq!(response.result.unwrap()["screensaver-active"], true);
session.set_locked(false); 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); let (sender, receiver) = std::sync::mpsc::sync_channel(1);
session.watch(1, 11, session.revision, sender); session.watch(1, 11, session.revision, sender);
session.remove(1); session.remove(1);
+41 -14
View File
@@ -26,7 +26,9 @@ struct Registration {
impl Registration { impl Registration {
fn timestamp(&self) -> u64 { 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) { fn release_all(&mut self) {
@@ -53,7 +55,11 @@ impl PortalShortcuts {
return Err("Shortcut limit reached".into()); return Err("Shortcut limit reached".into());
} }
let keymap = crate::config::physical_keymap(input).map_err(|error| error.to_string())?; 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()); return Err("Shortcut session has ended".into());
} }
let mut parsed: Vec<(Shortcut, Binding)> = Vec::new(); let mut parsed: Vec<(Shortcut, Binding)> = Vec::new();
@@ -62,11 +68,17 @@ impl PortalShortcuts {
.trigger .trigger
.split('+') .split('+')
.any(|part| part.trim().eq_ignore_ascii_case("num")); .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()); return Err("Invalid shortcut identifier".into());
} }
let binding = Binding::portal_trigger(&shortcut.trigger).map_err(|error| error.to_string())?; let binding =
if configured.iter().any(|other| binding.conflicts(other, &keymap)) Binding::portal_trigger(&shortcut.trigger).map_err(|error| error.to_string())?;
if configured
.iter()
.any(|other| binding.conflicts(other, &keymap))
|| parsed || parsed
.iter() .iter()
.any(|(other, key)| other.id == shortcut.id || binding.conflicts(key, &keymap)) .any(|(other, key)| other.id == shortcut.id || binding.conflicts(key, &keymap))
@@ -106,9 +118,9 @@ impl PortalShortcuts {
return; return;
}; };
for registration in self.0.values_mut() { for registration in self.0.values_mut() {
registration registration.shortcuts.retain(|(_, binding)| {
.shortcuts !configured.iter().any(|key| binding.conflicts(key, &keymap))
.retain(|(_, binding)| !configured.iter().any(|key| binding.conflicts(key, &keymap))); });
let active_ids = registration let active_ids = registration
.shortcuts .shortcuts
.iter() .iter()
@@ -144,7 +156,13 @@ 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() { for registration in self.0.values_mut() {
let Some((shortcut, _)) = registration.shortcuts.iter().find(|(shortcut, binding)| { let Some((shortcut, _)) = registration.shortcuts.iter().find(|(shortcut, binding)| {
(!shortcut.num_required || modifiers.num_lock) (!shortcut.num_required || modifiers.num_lock)
@@ -225,7 +243,10 @@ mod tests {
.register(1, vec![shortcut("one", "Super+q")], &configured, &input) .register(1, vec![shortcut("one", "Super+q")], &configured, &input)
.is_err() .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); shortcuts.remove(1);
assert_eq!(shortcuts.poll(1, false)["closed"], true); assert_eq!(shortcuts.poll(1, false)["closed"], true);
shortcuts shortcuts
@@ -274,7 +295,12 @@ mod tests {
.register(1, vec![shortcut("one", "Ctrl+Alt+a")], &[], &input) .register(1, vec![shortcut("one", "Ctrl+Alt+a")], &[], &input)
.unwrap(); .unwrap();
shortcuts.reconcile(&[Binding::portal_trigger("Ctrl+Alt+a").unwrap()], &input); 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+Escape").is_err());
assert!(Binding::portal_trigger("Ctrl+Alt+F3").is_err()); assert!(Binding::portal_trigger("Ctrl+Alt+F3").is_err());
} }
@@ -305,9 +331,10 @@ mod tests {
#[test] #[test]
fn physical_conflicts_include_secondary_layouts_and_num_modifier_is_honored() { fn physical_conflicts_include_secondary_layouts_and_num_modifier_is_honored() {
let config = let config = crate::config::Config::parse_source(
crate::config::Config::parse_source("binding keys=\"Ctrl+AD01\" match=\"physical\" action=\"none\"\n") "binding keys=\"Ctrl+AD01\" match=\"physical\" action=\"none\"\n",
.unwrap(); )
.unwrap();
let mut input = config.input_settings().unwrap(); let mut input = config.input_settings().unwrap();
input.xkb_layout = "us,fr".into(); input.xkb_layout = "us,fr".into();
let configured = config.bindings(&input).unwrap(); let configured = config.bindings(&input).unwrap();
+105 -26
View File
@@ -5,7 +5,9 @@ use std::time::{Duration, Instant};
use ferese_animation::AnimatedRect; use ferese_animation::AnimatedRect;
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, RenderElementStates}; use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, RenderElementStates};
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer, GlesTexProgram, GlesTexture, Uniform}; use smithay::backend::renderer::gles::{
GlesError, GlesFrame, GlesRenderer, GlesTexProgram, GlesTexture, Uniform,
};
use smithay::backend::renderer::utils::{CommitCounter, DamageSet}; use smithay::backend::renderer::utils::{CommitCounter, DamageSet};
use smithay::desktop::utils::OutputPresentationFeedback; use smithay::desktop::utils::OutputPresentationFeedback;
use smithay::output::Output; use smithay::output::Output;
@@ -52,7 +54,8 @@ impl WindowPresentation {
} }
pub fn close(&mut self) { pub fn close(&mut self) {
self.bounds.set_target(scaled_visual_rect(self.bounds.current, 0.98)); self.bounds
.set_target(scaled_visual_rect(self.bounds.current, 0.98));
self.opacity.set_target(0.0); self.opacity.set_target(0.0);
self.emphasis.set_target(0.0); self.emphasis.set_target(0.0);
self.shadow.set_target(0.0); self.shadow.set_target(0.0);
@@ -64,21 +67,32 @@ impl WindowPresentation {
/// always follows the window's shared rounded geometry. /// always follows the window's shared rounded geometry.
pub fn shadow_factors(self) -> (f64, f64, f64) { pub fn shadow_factors(self) -> (f64, f64, f64) {
let strength = self.shadow.current.clamp(0.0, 1.0); let strength = self.shadow.current.clamp(0.0, 1.0);
(0.75 + 0.25 * strength, 0.9 + 0.1 * strength, 0.8 + 0.2 * strength) (
0.75 + 0.25 * strength,
0.9 + 0.1 * strength,
0.8 + 0.2 * strength,
)
} }
} }
// Response multipliers preserve damping ratio and the user's crossing policy. // Response multipliers preserve damping ratio and the user's crossing policy.
// Emphasis responds sooner; the shadow settles a little later than the bounds. // Emphasis responds sooner; the shadow settles a little later than the bounds.
pub(crate) fn emphasis_spring(base: ferese_animation::SpringConfig) -> ferese_animation::SpringConfig { pub(crate) fn emphasis_spring(
base: ferese_animation::SpringConfig,
) -> ferese_animation::SpringConfig {
visual_response(base, 0.8) visual_response(base, 0.8)
} }
pub(crate) fn shadow_spring(base: ferese_animation::SpringConfig) -> ferese_animation::SpringConfig { pub(crate) fn shadow_spring(
base: ferese_animation::SpringConfig,
) -> ferese_animation::SpringConfig {
visual_response(base, 1.2) visual_response(base, 1.2)
} }
fn visual_response(base: ferese_animation::SpringConfig, response: f64) -> ferese_animation::SpringConfig { fn visual_response(
base: ferese_animation::SpringConfig,
response: f64,
) -> ferese_animation::SpringConfig {
ferese_animation::SpringConfig { ferese_animation::SpringConfig {
stiffness: base.stiffness / (response * response), stiffness: base.stiffness / (response * response),
damping: base.damping / response, damping: base.damping / response,
@@ -95,11 +109,14 @@ pub(crate) fn take_output_feedback(
flags: wp_presentation_feedback::Kind, flags: wp_presentation_feedback::Kind,
) -> OutputPresentationFeedback { ) -> OutputPresentationFeedback {
use smithay::desktop::layer_map_for_output; use smithay::desktop::layer_map_for_output;
use smithay::desktop::utils::{OutputPresentationFeedback, surface_presentation_feedback_flags_from_states}; use smithay::desktop::utils::{
OutputPresentationFeedback, surface_presentation_feedback_flags_from_states,
};
let mut feedback = OutputPresentationFeedback::new(output); let mut feedback = OutputPresentationFeedback::new(output);
let visible_output = let visible_output = |surface: &WlSurface, _: &SurfaceData| {
|surface: &WlSurface, _: &SurfaceData| rendered_feedback_output(output, surface.into(), rendered); rendered_feedback_output(output, surface.into(), rendered)
};
let surface_flags = |surface: &WlSurface, _: &SurfaceData| { let surface_flags = |surface: &WlSurface, _: &SurfaceData| {
flags | surface_presentation_feedback_flags_from_states(surface, None, rendered) flags | surface_presentation_feedback_flags_from_states(surface, None, rendered)
}; };
@@ -121,7 +138,11 @@ pub(crate) fn take_output_feedback(
feedback feedback
} }
fn rendered_feedback_output(output: &Output, id: Id, rendered: &RenderElementStates) -> Option<Output> { fn rendered_feedback_output(
output: &Output,
id: Id,
rendered: &RenderElementStates,
) -> Option<Output> {
rendered rendered
.element_render_state(id) .element_render_state(id)
.is_some_and(crate::display_presentation::is_visible) .is_some_and(crate::display_presentation::is_visible)
@@ -153,7 +174,11 @@ pub(crate) fn physical_rect(
pub(crate) use ferese_shape::Shape as CornerShape; pub(crate) use ferese_shape::Shape as CornerShape;
/// Conservative extent of the corner shoulders, in physical pixels. /// Conservative extent of the corner shoulders, in physical pixels.
pub(crate) fn corner_extent(radius: f32, size: smithay::utils::Size<i32, Physical>, shape: CornerShape) -> f32 { pub(crate) fn corner_extent(
radius: f32,
size: smithay::utils::Size<i32, Physical>,
shape: CornerShape,
) -> f32 {
if shape == CornerShape::Circular { if shape == CornerShape::Circular {
return radius; return radius;
} }
@@ -175,7 +200,12 @@ impl RoundedRect {
self self
} }
pub(crate) fn new(rect: ferese_layout::Rect, origin: Point<i32, Logical>, scale: f64, radius: f64) -> Self { pub(crate) fn new(
rect: ferese_layout::Rect,
origin: Point<i32, Logical>,
scale: f64,
radius: f64,
) -> Self {
let rect = physical_rect(rect, origin, scale); let rect = physical_rect(rect, origin, scale);
Self { Self {
radius: clamp_radius(radius * scale, rect.size), radius: clamp_radius(radius * scale, rect.size),
@@ -271,7 +301,11 @@ impl Element for PhysicalShaderElement {
self.geometry 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()) { if commit == Some(self.current_commit()) {
DamageSet::default() DamageSet::default()
} else { } else {
@@ -333,7 +367,11 @@ impl Element for NativeTextureElement {
Kind::Unspecified 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) { if commit == Some(self.commit) {
DamageSet::default() DamageSet::default()
} else { } else {
@@ -454,7 +492,8 @@ mod tests {
fn layered_responses_preserve_damping_and_settle_independently() { fn layered_responses_preserve_damping_and_settle_independently() {
use ferese_animation::{AnimatedValue, SpringConfig}; use ferese_animation::{AnimatedValue, SpringConfig};
let base = SpringConfig::default(); let base = SpringConfig::default();
let damping_ratio = |spring: SpringConfig| spring.damping / (2.0 * (spring.mass * spring.stiffness).sqrt()); let damping_ratio =
|spring: SpringConfig| spring.damping / (2.0 * (spring.mass * spring.stiffness).sqrt());
for spring in [emphasis_spring(base), shadow_spring(base)] { for spring in [emphasis_spring(base), shadow_spring(base)] {
assert!((damping_ratio(spring) - damping_ratio(base)).abs() < 1e-12); assert!((damping_ratio(spring) - damping_ratio(base)).abs() < 1e-12);
assert_eq!(spring.crossing, base.crossing); assert_eq!(spring.crossing, base.crossing);
@@ -482,7 +521,12 @@ mod tests {
#[test] #[test]
fn rounded_outline_clamps_after_physical_snapping() { fn rounded_outline_clamps_after_physical_snapping() {
// Logical clamping would give 1.125; the snapped 3px rect allows radius 1.5. // Logical clamping would give 1.125; the snapped 3px rect allows radius 1.5.
let outline = RoundedRect::new(ferese_layout::Rect::new(0.2, 0.2, 1.5, 4.0), (0, 0).into(), 1.5, 20.0); let outline = RoundedRect::new(
ferese_layout::Rect::new(0.2, 0.2, 1.5, 4.0),
(0, 0).into(),
1.5,
20.0,
);
assert_eq!(outline.rect, Rectangle::new((0, 0).into(), (3, 6).into())); assert_eq!(outline.rect, Rectangle::new((0, 0).into(), (3, 6).into()));
assert_eq!(outline.radius, 1.5); assert_eq!(outline.radius, 1.5);
for scale in [1.0, 1.25, 1.5, 2.0] { for scale in [1.0, 1.25, 1.5, 2.0] {
@@ -504,7 +548,12 @@ mod tests {
assert_eq!(clamp_radius(0.0, size), 0.0); assert_eq!(clamp_radius(0.0, size), 0.0);
assert_eq!(clamp_radius(-1.0, size), 0.0); assert_eq!(clamp_radius(-1.0, size), 0.0);
assert_eq!(clamp_radius(2.0, (0, 10).into()), 0.0); assert_eq!(clamp_radius(2.0, (0, 10).into()), 0.0);
let outline = RoundedRect::new(ferese_layout::Rect::new(0.0, 0.0, 10.0, 10.0), (0, 0).into(), 1.5, 1.5); let outline = RoundedRect::new(
ferese_layout::Rect::new(0.0, 0.0, 10.0, 10.0),
(0, 0).into(),
1.5,
1.5,
);
assert_eq!(outline.radius, 2.25); assert_eq!(outline.radius, 2.25);
} }
@@ -528,7 +577,9 @@ mod tests {
#[test] #[test]
fn presentation_feedback_excludes_missing_and_occluded_surfaces() { fn presentation_feedback_excludes_missing_and_occluded_surfaces() {
use smithay::backend::renderer::element::{RenderElementPresentationState, RenderElementState}; use smithay::backend::renderer::element::{
RenderElementPresentationState, RenderElementState,
};
use smithay::output::{PhysicalProperties, Subpixel}; use smithay::output::{PhysicalProperties, Subpixel};
let output = Output::new( let output = Output::new(
@@ -580,9 +631,24 @@ mod tests {
#[test] #[test]
fn reversing_a_grow_replaces_the_smaller_snapshot() { 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(
assert!(snapshot_covers_source((1000, 600).into(), (500, 600).into(), 1.0, 1.0)); (500, 600).into(),
assert!(!snapshot_covers_source((1000, 600).into(), (500, 600).into(), 1.0, 1.8)); (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] #[test]
@@ -660,11 +726,13 @@ mod tests {
fast.callback_sent(start); fast.callback_sent(start);
fast.callback_sent(start + Duration::from_millis(8)); fast.callback_sent(start + Duration::from_millis(8));
assert_eq!( assert_eq!(
slow.callback_deadline(start + Duration::from_millis(8), start).unwrap(), slow.callback_deadline(start + Duration::from_millis(8), start)
.unwrap(),
start + Duration::from_millis(16) start + Duration::from_millis(16)
); );
assert_eq!( assert_eq!(
fast.callback_deadline(start + Duration::from_millis(8), start).unwrap(), fast.callback_deadline(start + Duration::from_millis(8), start)
.unwrap(),
start + Duration::from_millis(16) start + Duration::from_millis(16)
); );
} }
@@ -686,14 +754,25 @@ mod tests {
fn fractional_motion_does_not_quantize_to_logical_pixels() { fn fractional_motion_does_not_quantize_to_logical_pixels() {
let rect = ferese_layout::Rect::new(0.3, 0.3, 100.2, 80.2); 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).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] #[test]
fn adjacent_frames_share_edges_at_all_output_scales() { fn adjacent_frames_share_edges_at_all_output_scales() {
for scale in [1.0, 1.25, 1.5, 1.8, 2.0] { 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 left = physical_rect(
let right = physical_rect(ferese_layout::Rect::new(513.7, 0.0, 500.4, 800.0), (0, 0).into(), scale); 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); assert_eq!(left.loc.x + left.size.w, right.loc.x);
} }
} }
+14 -5
View File
@@ -15,7 +15,8 @@ fn mapped_tree(surface: &WlSurface) -> bool {
let mut current = Some(surface.clone()); let mut current = Some(surface.clone());
while let Some(surface) = current { while let Some(surface) = current {
if !surface.is_alive() if !surface.is_alive()
|| !with_renderer_surface_state(&surface, |state| state.buffer().is_some()).unwrap_or(false) || !with_renderer_surface_state(&surface, |state| state.buffer().is_some())
.unwrap_or(false)
{ {
return false; return false;
} }
@@ -29,9 +30,12 @@ impl Ferese {
let mut surfaces = Vec::new(); let mut surfaces = Vec::new();
if self.session_lock.active() { if self.session_lock.active() {
for lock in self.session_lock.surfaces.values() { for lock in self.session_lock.surfaces.values() {
smithay::desktop::utils::with_surfaces_surface_tree(lock.wl_surface(), |surface, _| { smithay::desktop::utils::with_surfaces_surface_tree(
surfaces.push(surface.clone()); lock.wl_surface(),
}); |surface, _| {
surfaces.push(surface.clone());
},
);
} }
} else { } else {
for window in self.space.elements() { for window in self.space.elements() {
@@ -55,7 +59,12 @@ impl Ferese {
let output = self let output = self
.display_presentation .display_presentation
.outputs_for(&id) .outputs_for(&id)
.max_by_key(|output| (output.current_mode().map_or(0, |mode| mode.refresh), output.name()))? .max_by_key(|output| {
(
output.current_mode().map_or(0, |mode| mode.refresh),
output.name(),
)
})?
.clone(); .clone();
Some((id, output)) Some((id, output))
}) })
+10 -4
View File
@@ -34,8 +34,8 @@ pub(crate) fn prepare_command(
command: &mut Command, command: &mut Command,
capabilities: ClientCapabilities, capabilities: ClientCapabilities,
) -> io::Result<Vec<UnixStream>> { ) -> io::Result<Vec<UnixStream>> {
let split_shell_control = let split_shell_control = capabilities.contains(ClientCapabilities::EFFECTS)
capabilities.contains(ClientCapabilities::EFFECTS) && capabilities.contains(ClientCapabilities::SHELL_CONTROL); && capabilities.contains(ClientCapabilities::SHELL_CONTROL);
let primary_capabilities = if split_shell_control { let primary_capabilities = if split_shell_control {
ClientCapabilities::EFFECTS ClientCapabilities::EFFECTS
} else { } else {
@@ -54,7 +54,10 @@ pub(crate) fn prepare_command(
if split_shell_control { if split_shell_control {
let control = insert_private_client(state, ClientCapabilities::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); clients.push(control);
} }
@@ -76,7 +79,10 @@ pub(crate) fn prepare_command(
Ok(clients) 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()?; let (server, client) = UnixStream::pair()?;
state.display_handle.insert_client( state.display_handle.insert_client(
+24 -15
View File
@@ -133,32 +133,41 @@ fn reap(child: &RefCell<Option<Child>>) -> bool {
} }
} }
pub(crate) fn watch_client_exit(handle: &LoopHandle<'static, Ferese>, child: Child) -> Rc<RefCell<Option<Child>>> { pub(crate) fn watch_client_exit(
handle: &LoopHandle<'static, Ferese>,
child: Child,
) -> Rc<RefCell<Option<Child>>> {
let pid = child.id(); let pid = child.id();
let owner = Rc::new(RefCell::new(Some(child))); let owner = Rc::new(RefCell::new(Some(child)));
let watched = owner.clone(); let watched = owner.clone();
let result = pidfd(pid).and_then(|fd| { let result = pidfd(pid).and_then(|fd| {
handle handle
.insert_source(Generic::new(fd, Interest::READ, Mode::Level), move |_, _, _| { .insert_source(
Ok(if reap(&watched) { Generic::new(fd, Interest::READ, Mode::Level),
PostAction::Remove move |_, _, _| {
} else { Ok(if reap(&watched) {
PostAction::Continue PostAction::Remove
}) } else {
}) PostAction::Continue
})
},
)
.map_err(io::Error::other) .map_err(io::Error::other)
}); });
if let Err(error) = result { if let Err(error) = result {
warn!(%error, pid, "could not watch client exit with pidfd; polling instead"); warn!(%error, pid, "could not watch client exit with pidfd; polling instead");
let watched = owner.clone(); let watched = owner.clone();
if let Err(error) = handle.insert_source(Timer::from_duration(Duration::from_millis(250)), move |_, _, _| { if let Err(error) = handle.insert_source(
if reap(&watched) { Timer::from_duration(Duration::from_millis(250)),
TimeoutAction::Drop move |_, _, _| {
} else { if reap(&watched) {
TimeoutAction::ToDuration(Duration::from_millis(250)) TimeoutAction::Drop
} } else {
}) { TimeoutAction::ToDuration(Duration::from_millis(250))
}
},
) {
warn!(%error, pid, "could not schedule client exit polling; stopping child"); warn!(%error, pid, "could not schedule client exit polling; stopping child");
if let Some(mut child) = owner.borrow_mut().take() { if let Some(mut child) = owner.borrow_mut().take() {
terminate_child(&mut child); terminate_child(&mut child);
+56 -32
View File
@@ -51,13 +51,16 @@ impl Supervisor {
let watched = pidfd(pid).and_then(|fd| { let watched = pidfd(pid).and_then(|fd| {
state state
.loop_handle .loop_handle
.insert_source(Generic::new(fd, Interest::READ, Mode::Level), move |_, _, state| { .insert_source(
if let Some(owner) = weak.upgrade() { Generic::new(fd, Interest::READ, Mode::Level),
owner.borrow_mut().source = None; move |_, _, state| {
Self::exited(&owner, state); if let Some(owner) = weak.upgrade() {
} owner.borrow_mut().source = None;
Ok(PostAction::Remove) Self::exited(&owner, state);
}) }
Ok(PostAction::Remove)
},
)
.map_err(io::Error::other) .map_err(io::Error::other)
}); });
if let Ok(token) = watched { if let Ok(token) = watched {
@@ -69,23 +72,26 @@ impl Supervisor {
let weak = Rc::downgrade(owner); let weak = Rc::downgrade(owner);
let token = state let token = state
.loop_handle .loop_handle
.insert_source(Timer::from_duration(Duration::from_millis(250)), move |_, _, state| { .insert_source(
let Some(owner) = weak.upgrade() else { Timer::from_duration(Duration::from_millis(250)),
return TimeoutAction::Drop; move |_, _, state| {
}; let Some(owner) = weak.upgrade() else {
let done = owner return TimeoutAction::Drop;
.borrow_mut() };
.child let done = owner
.as_mut() .borrow_mut()
.is_none_or(|child| child.try_wait().ok().flatten().is_some()); .child
if done { .as_mut()
owner.borrow_mut().source = None; .is_none_or(|child| child.try_wait().ok().flatten().is_some());
Self::exited(&owner, state); if done {
TimeoutAction::Drop owner.borrow_mut().source = None;
} else { Self::exited(&owner, state);
TimeoutAction::ToDuration(Duration::from_millis(250)) TimeoutAction::Drop
} } else {
}) TimeoutAction::ToDuration(Duration::from_millis(250))
}
},
)
.expect("register desktop child watcher"); .expect("register desktop child watcher");
owner.borrow_mut().source = Some(token); owner.borrow_mut().source = Some(token);
} else { } else {
@@ -177,7 +183,8 @@ mod tests {
if !crate::startup_tests::private_runtime(test) { if !crate::startup_tests::private_runtime(test) {
return; return;
} }
crate::xwayland::test_hooks::PIDFD_UNAVAILABLE.store(polling, std::sync::atomic::Ordering::SeqCst); crate::xwayland::test_hooks::PIDFD_UNAVAILABLE
.store(polling, std::sync::atomic::Ordering::SeqCst);
let mut events = calloop::EventLoop::try_new().unwrap(); let mut events = calloop::EventLoop::try_new().unwrap();
let mut state = crate::startup_tests::state(&mut events); let mut state = crate::startup_tests::state(&mut events);
let directory = tempfile::tempdir().unwrap(); let directory = tempfile::tempdir().unwrap();
@@ -200,30 +207,42 @@ if os.environ['FERESE_CLIENT_RESTART'] == '1':
); );
let deadline = Instant::now() + Duration::from_secs(5); let deadline = Instant::now() + Duration::from_secs(5);
let rows = loop { let rows = loop {
events.dispatch(Duration::from_millis(20), &mut state).unwrap(); events
.dispatch(Duration::from_millis(20), &mut state)
.unwrap();
let text = std::fs::read_to_string(&record).unwrap_or_default(); let text = std::fs::read_to_string(&record).unwrap_or_default();
let rows: Vec<_> = text.lines().map(str::to_owned).collect(); let rows: Vec<_> = text.lines().map(str::to_owned).collect();
if rows.len() == 2 { if rows.len() == 2 {
break rows; break rows;
} }
assert!(Instant::now() < deadline, "supervised child did not restart: {text}"); assert!(
Instant::now() < deadline,
"supervised child did not restart: {text}"
);
}; };
assert!(rows[0].ends_with(" 0")); assert!(rows[0].ends_with(" 0"));
assert!(rows[1].ends_with(" 1")); assert!(rows[1].ends_with(" 1"));
assert_ne!(rows[0].split_whitespace().next(), rows[1].split_whitespace().next()); assert_ne!(
rows[0].split_whitespace().next(),
rows[1].split_whitespace().next()
);
let child_pid = owner.borrow().child.as_ref().unwrap().id(); let child_pid = owner.borrow().child.as_ref().unwrap().id();
unsafe { unsafe {
libc::kill(child_pid as i32, libc::SIGKILL); libc::kill(child_pid as i32, libc::SIGKILL);
} }
while owner.borrow().child.is_some() { while owner.borrow().child.is_some() {
events.dispatch(Duration::from_millis(20), &mut state).unwrap(); events
.dispatch(Duration::from_millis(20), &mut state)
.unwrap();
assert!(Instant::now() < deadline); assert!(Instant::now() < deadline);
} }
assert!(owner.borrow().source.is_some(), "restart timer armed"); assert!(owner.borrow().source.is_some(), "restart timer armed");
owner.borrow_mut().stop(&state.loop_handle); owner.borrow_mut().stop(&state.loop_handle);
let stopped = Instant::now(); let stopped = Instant::now();
while stopped.elapsed() < Duration::from_millis(2200) { while stopped.elapsed() < Duration::from_millis(2200) {
events.dispatch(Duration::from_millis(20), &mut state).unwrap(); events
.dispatch(Duration::from_millis(20), &mut state)
.unwrap();
} }
assert_eq!(std::fs::read_to_string(record).unwrap().lines().count(), 2); assert_eq!(std::fs::read_to_string(record).unwrap().lines().count(), 2);
assert!(owner.borrow().source.is_none()); assert!(owner.borrow().source.is_none());
@@ -265,7 +284,9 @@ time.sleep(60)
break pid; break pid;
} }
assert!(Instant::now() < deadline); assert!(Instant::now() < deadline);
events.dispatch(Duration::from_millis(20), &mut state).unwrap(); events
.dispatch(Duration::from_millis(20), &mut state)
.unwrap();
}; };
owner.borrow_mut().stop(&state.loop_handle); owner.borrow_mut().stop(&state.loop_handle);
let status_path = format!("/proc/{pid}/status"); let status_path = format!("/proc/{pid}/status");
@@ -279,7 +300,10 @@ time.sleep(60)
if !running { if !running {
break; break;
} }
assert!(Instant::now() < deadline, "helper descendant survived shutdown"); assert!(
Instant::now() < deadline,
"helper descendant survived shutdown"
);
std::thread::sleep(Duration::from_millis(10)); std::thread::sleep(Duration::from_millis(10));
} }
assert!(owner.borrow().child.is_none()); assert!(owner.borrow().child.is_none());
+103 -45
View File
@@ -14,14 +14,19 @@ impl Prepared {
Self::with_watch(source, directory, &mut Vec::new()) Self::with_watch(source, directory, &mut Vec::new())
} }
fn with_watch(source: String, directory: &std::path::Path, watched: &mut Vec<PathBuf>) -> Result<Self, String> { fn with_watch(
source: String,
directory: &std::path::Path,
watched: &mut Vec<PathBuf>,
) -> Result<Self, String> {
if source.len() > 60 * 1024 { if source.len() > 60 * 1024 {
return Err("configuration exceeds the 60 KiB live-reload limit".into()); return Err("configuration exceeds the 60 KiB live-reload limit".into());
} }
let document = ferese_config::Document::parse(&source).map_err(|e| e.to_string())?; let document = ferese_config::Document::parse(&source).map_err(|e| e.to_string())?;
if let Some(value) = document.get("theme") if let Some(value) = document.get("theme")
&& let Ok(policy) = serde_json::from_value::<ferese_config::theme::Policy>(value.clone()) && let Ok(policy) =
serde_json::from_value::<ferese_config::theme::Policy>(value.clone())
{ {
watched.extend( watched.extend(
[&policy.file, &policy.light.file, &policy.dark.file] [&policy.file, &policy.light.file, &policy.dark.file]
@@ -76,24 +81,27 @@ impl Worker {
event_loop: &mut calloop::EventLoop<'static, crate::Ferese>, event_loop: &mut calloop::EventLoop<'static, crate::Ferese>,
) -> Result<Self, Box<dyn std::error::Error>> { ) -> Result<Self, Box<dyn std::error::Error>> {
let (requests, receiver) = std::sync::mpsc::sync_channel::<Request>(8); let (requests, receiver) = std::sync::mpsc::sync_channel::<Request>(8);
let (results, events) = calloop::channel::channel::<(Result<Prepared, String>, Vec<PathBuf>, Option<Reply>)>(); let (results, events) =
event_loop.handle().insert_source(events, |event, _, state| { calloop::channel::channel::<(Result<Prepared, String>, Vec<PathBuf>, Option<Reply>)>();
if let calloop::channel::Event::Msg((result, watched, reply)) = event { event_loop
state.theme_engine.watch_files(&watched); .handle()
let result = result.and_then(|prepared| state.apply_prepared_config(prepared)); .insert_source(events, |event, _, state| {
if let Err(error) = &result { if let calloop::channel::Event::Msg((result, watched, reply)) = event {
state.theme_engine.reject(error.clone()); state.theme_engine.watch_files(&watched);
} let result = result.and_then(|prepared| state.apply_prepared_config(prepared));
if let Err(error) = &result {
state.theme_engine.reject(error.clone());
}
if let Some((id, reply)) = reply { if let Some((id, reply)) = reply {
let response = match result { let response = match result {
Ok(()) => ferese_ipc::Response::success(id, serde_json::json!({})), Ok(()) => ferese_ipc::Response::success(id, serde_json::json!({})),
Err(error) => ferese_ipc::Response::error(id, "invalid_config", error), Err(error) => ferese_ipc::Response::error(id, "invalid_config", error),
}; };
let _ = reply.try_send(response); let _ = reply.try_send(response);
}
} }
} })?;
})?;
std::thread::Builder::new() std::thread::Builder::new()
.name("ferese-config".into()) .name("ferese-config".into())
.spawn(move || { .spawn(move || {
@@ -110,7 +118,11 @@ impl Worker {
let result = path let result = path
.as_ref() .as_ref()
.map_or_else( .map_or_else(
|| request.fallback.ok_or_else(|| "No configuration available".into()), || {
request
.fallback
.ok_or_else(|| "No configuration available".into())
},
|path| { |path| {
if path.exists() || !request.force { if path.exists() || !request.force {
crate::config::read_source(path) crate::config::read_source(path)
@@ -124,7 +136,8 @@ impl Worker {
// Auto mode still resolves time and system appearance. // Auto mode still resolves time and system appearance.
if let Some(previous) = &cached if let Some(previous) = &cached
&& previous.source == source && previous.source == source
&& previous.candidate.policy.mode != ferese_config::theme::Mode::Auto && previous.candidate.policy.mode
!= ferese_config::theme::Mode::Auto
&& file_stamps(&previous.candidate.files) == stamps && file_stamps(&previous.candidate.files) == stamps
{ {
return Ok(previous.clone()); return Ok(previous.clone());
@@ -149,16 +162,23 @@ impl Worker {
} }
impl crate::Ferese { impl crate::Ferese {
pub(crate) fn queue_config_reload(&self, force: bool, reply: Option<Reply>) -> Result<(), String> { pub(crate) fn queue_config_reload(
&self,
force: bool,
reply: Option<Reply>,
) -> Result<(), String> {
let request = Request { let request = Request {
force, force,
fallback: self.config_source.clone(), fallback: self.config_source.clone(),
reply, reply,
}; };
let worker = self.config_worker.as_ref().ok_or("configuration worker unavailable")?; let worker = self
.config_worker
.as_ref()
.ok_or("configuration worker unavailable")?;
if let Err(error) = worker.requests.try_send(request) { if let Err(error) = worker.requests.try_send(request) {
let (std::sync::mpsc::TrySendError::Full(request) | std::sync::mpsc::TrySendError::Disconnected(request)) = let (std::sync::mpsc::TrySendError::Full(request)
error; | std::sync::mpsc::TrySendError::Disconnected(request)) = error;
if let Some((id, reply)) = request.reply { if let Some((id, reply)) = request.reply {
let _ = reply.try_send(ferese_ipc::Response::error( let _ = reply.try_send(ferese_ipc::Response::error(
id, id,
@@ -276,8 +296,9 @@ mod tests {
#[test] #[test]
fn wallpaper_only_reload_applies_path_and_mode_changes() { fn wallpaper_only_reload_applies_path_and_mode_changes() {
if !crate::startup_tests::private_runtime("reload::tests::wallpaper_only_reload_applies_path_and_mode_changes") if !crate::startup_tests::private_runtime(
{ "reload::tests::wallpaper_only_reload_applies_path_and_mode_changes",
) {
return; return;
} }
let mut events = calloop::EventLoop::try_new().unwrap(); let mut events = calloop::EventLoop::try_new().unwrap();
@@ -286,8 +307,12 @@ mod tests {
let image_path = directory.path().join("wallpaper.png"); let image_path = directory.path().join("wallpaper.png");
image::RgbaImage::new(2, 2).save(&image_path).unwrap(); image::RgbaImage::new(2, 2).save(&image_path).unwrap();
for mode in ["fit", "fill"] { for mode in ["fit", "fill"] {
let source = format!("wallpaper {{ path \"{}\"; mode \"{mode}\"; }}", image_path.display()); let source = format!(
let (_, runtime, candidate) = crate::config::prepare(&source, directory.path()).unwrap(); "wallpaper {{ path \"{}\"; mode \"{mode}\"; }}",
image_path.display()
);
let (_, runtime, candidate) =
crate::config::prepare(&source, directory.path()).unwrap();
// Model a settled theme; changing only the wallpaper must still apply. // Model a settled theme; changing only the wallpaper must still apply.
state.theme_engine.live.theme = candidate.theme.clone(); state.theme_engine.live.theme = candidate.theme.clone();
state.theme_engine.live.presented = candidate.theme; state.theme_engine.live.presented = candidate.theme;
@@ -310,8 +335,9 @@ mod tests {
#[test] #[test]
fn rejected_keymap_reload_preserves_other_configuration() { fn rejected_keymap_reload_preserves_other_configuration() {
if !crate::startup_tests::private_runtime("reload::tests::rejected_keymap_reload_preserves_other_configuration") if !crate::startup_tests::private_runtime(
{ "reload::tests::rejected_keymap_reload_preserves_other_configuration",
) {
return; return;
} }
let mut events = calloop::EventLoop::try_new().unwrap(); let mut events = calloop::EventLoop::try_new().unwrap();
@@ -326,7 +352,9 @@ mod tests {
// Exercise application failure after preparation, for example when an // Exercise application failure after preparation, for example when an
// XKB file disappears before the compositor installs the new keymap. // XKB file disappears before the compositor installs the new keymap.
runtime.input_settings.xkb_layout = "ferese_missing_keymap_for_reload_test".into(); runtime.input_settings.xkb_layout = "ferese_missing_keymap_for_reload_test".into();
let error = state.apply_runtime_config(runtime, &serde_json::json!({})).unwrap_err(); let error = state
.apply_runtime_config(runtime, &serde_json::json!({}))
.unwrap_err();
assert!(error.contains("keymap reload failed"), "{error}"); assert!(error.contains("keymap reload failed"), "{error}");
assert_eq!(state.xwayland_config.enabled, before_xwayland); assert_eq!(state.xwayland_config.enabled, before_xwayland);
assert_eq!(state.activities.snapshot().revision, before_activities); assert_eq!(state.activities.snapshot().revision, before_activities);
@@ -336,9 +364,14 @@ mod tests {
let mut runtime = crate::config::Config::default().runtime_config().unwrap(); let mut runtime = crate::config::Config::default().runtime_config().unwrap();
runtime.xwayland.enabled = !before_xwayland; runtime.xwayland.enabled = !before_xwayland;
runtime.layout_mode = ferese_core::LayoutMode::Tree; runtime.layout_mode = ferese_core::LayoutMode::Tree;
state.apply_runtime_config(runtime, &serde_json::json!({})).unwrap(); state
.apply_runtime_config(runtime, &serde_json::json!({}))
.unwrap();
assert_eq!(state.xwayland_config.enabled, !before_xwayland); assert_eq!(state.xwayland_config.enabled, !before_xwayland);
assert_eq!(state.workspaces.active().layout.mode(), ferese_core::LayoutMode::Tree); assert_eq!(
state.workspaces.active().layout.mode(),
ferese_core::LayoutMode::Tree
);
assert!(state.activities.snapshot().revision > before_activities); assert!(state.activities.snapshot().revision > before_activities);
} }
@@ -379,9 +412,14 @@ mod tests {
std::fs::create_dir_all(path.parent().unwrap()).unwrap(); std::fs::create_dir_all(path.parent().unwrap()).unwrap();
let valid = "animations { speed 0.5; }"; let valid = "animations { speed 0.5; }";
std::fs::write(&path, valid).unwrap(); std::fs::write(&path, valid).unwrap();
for (index, source) in [Some(valid), Some(valid), Some("animations { speed -1.0; }"), None] for (index, source) in [
.into_iter() Some(valid),
.enumerate() Some(valid),
Some("animations { speed -1.0; }"),
None,
]
.into_iter()
.enumerate()
{ {
if let Some(source) = source { if let Some(source) = source {
std::fs::write(&path, source).unwrap(); std::fs::write(&path, source).unwrap();
@@ -392,7 +430,9 @@ mod tests {
let before = state.ipc_events.config_revision; let before = state.ipc_events.config_revision;
let theme_before = state.theme_engine.live.theme.clone(); let theme_before = state.theme_engine.live.theme.clone();
let (reply, received) = std::sync::mpsc::sync_channel(1); let (reply, received) = std::sync::mpsc::sync_channel(1);
state.queue_config_reload(index == 1, Some((42, reply))).unwrap(); state
.queue_config_reload(index == 1, Some((42, reply)))
.unwrap();
if index == 1 { if index == 1 {
// Present a frame while the duplicate reload is queued. Theme // Present a frame while the duplicate reload is queued. Theme
// revisions include these frames; config revisions do not. // revisions include these frames; config revisions do not.
@@ -401,7 +441,9 @@ mod tests {
} }
let deadline = Instant::now() + Duration::from_secs(5); let deadline = Instant::now() + Duration::from_secs(5);
let response = loop { let response = loop {
event_loop.dispatch(Duration::from_millis(10), &mut state).unwrap(); event_loop
.dispatch(Duration::from_millis(10), &mut state)
.unwrap();
if let Ok(response) = received.try_recv() { if let Ok(response) = received.try_recv() {
break response; break response;
} }
@@ -415,7 +457,10 @@ mod tests {
); );
assert_eq!(state.config_source.as_deref(), Some(valid)); assert_eq!(state.config_source.as_deref(), Some(valid));
let applied = source == Some(valid) && !already_applied; let applied = source == Some(valid) && !already_applied;
assert_eq!(state.ipc_events.config_revision, before + u64::from(applied)); assert_eq!(
state.ipc_events.config_revision,
before + u64::from(applied)
);
if already_applied { if already_applied {
assert_eq!(state.theme_engine.live.theme, theme_before); assert_eq!(state.theme_engine.live.theme, theme_before);
} }
@@ -460,8 +505,9 @@ mod tests {
state.theme_engine.live.presented = candidate.theme; state.theme_engine.live.presented = candidate.theme;
for inactive_opacity in [1.0, 0.75, 0.9] { for inactive_opacity in [1.0, 0.75, 0.9] {
let source = let source = format!(
format!("appearance {{ focus-effect {{ inactive-opacity {inactive_opacity}; inactive-dim 0.25; }}; }}"); "appearance {{ focus-effect {{ inactive-opacity {inactive_opacity}; inactive-dim 0.25; }}; }}"
);
state.reload_config_source(source).unwrap(); state.reload_config_source(source).unwrap();
assert_eq!(state.focus_effect.inactive_opacity, inactive_opacity); assert_eq!(state.focus_effect.inactive_opacity, inactive_opacity);
let (alpha, dim) = state.focus_effect.resolve(0.0, true, 0.0); let (alpha, dim) = state.focus_effect.resolve(0.0, true, 0.0);
@@ -486,7 +532,10 @@ mod tests {
); );
assert_eq!(state.focus_effect, previous); assert_eq!(state.focus_effect, previous);
state state
.reload_config_source("appearance { focus-effect { inactive-dim 0; }; }; theme { mode \"light\"; }".into()) .reload_config_source(
"appearance { focus-effect { inactive-dim 0; }; }; theme { mode \"light\"; }"
.into(),
)
.unwrap(); .unwrap();
state.poll_theme(); state.poll_theme();
assert_eq!(state.focus_effect.inactive_dim, 0.0); assert_eq!(state.focus_effect.inactive_dim, 0.0);
@@ -514,12 +563,17 @@ mod tests {
.env_remove("FERESE_SOCKET") .env_remove("FERESE_SOCKET")
.output() .output()
.unwrap(); .unwrap();
assert!(result.status.success(), "{}", String::from_utf8_lossy(&result.stderr)); assert!(
result.status.success(),
"{}",
String::from_utf8_lossy(&result.stderr)
);
return; return;
} }
let directory = Directory::new(); let directory = Directory::new();
let (_, runtime, _) = crate::config::prepare("xwayland { enabled #false; }", &directory.0).unwrap(); let (_, runtime, _) =
crate::config::prepare("xwayland { enabled #false; }", &directory.0).unwrap();
let mut event_loop = smithay::reexports::calloop::EventLoop::try_new().unwrap(); let mut event_loop = smithay::reexports::calloop::EventLoop::try_new().unwrap();
let display = smithay::reexports::wayland_server::Display::new().unwrap(); let display = smithay::reexports::wayland_server::Display::new().unwrap();
let mut state = crate::Ferese::new(&mut event_loop, display, runtime).unwrap(); let mut state = crate::Ferese::new(&mut event_loop, display, runtime).unwrap();
@@ -563,7 +617,11 @@ mod tests {
.env_remove("FERESE_SOCKET") .env_remove("FERESE_SOCKET")
.output() .output()
.unwrap(); .unwrap();
assert!(result.status.success(), "{}", String::from_utf8_lossy(&result.stderr)); assert!(
result.status.success(),
"{}",
String::from_utf8_lossy(&result.stderr)
);
return; return;
} }
let directory = Directory::new(); let directory = Directory::new();
+61 -13
View File
@@ -39,7 +39,10 @@ impl Backdrop {
continue; continue;
} }
let previous = self.sources.iter().find(|source| &source.id == element.id()); let previous = self
.sources
.iter()
.find(|source| &source.id == element.id());
changed |= previous.is_none_or(|source| { changed |= previous.is_none_or(|source| {
source.geometry != geometry source.geometry != geometry
|| source.alpha != element.alpha() || source.alpha != element.alpha()
@@ -48,10 +51,13 @@ impl Backdrop {
|| source.visible != visible || source.visible != visible
}); });
if let Some(previous) = previous { if let Some(previous) = previous {
changed |= element.damage_since(scale, Some(previous.commit)).iter().any(|damage| { changed |= element
let damage = Rectangle::new(damage.loc + geometry.loc, damage.size); .damage_since(scale, Some(previous.commit))
visible.iter().any(|rect| rect.overlaps(damage)) .iter()
}); .any(|damage| {
let damage = Rectangle::new(damage.loc + geometry.loc, damage.size);
visible.iter().any(|rect| rect.overlaps(damage))
});
} }
sources.push(Source { sources.push(Source {
@@ -163,7 +169,9 @@ mod tests {
} }
fn current_commit(&self) -> CommitCounter { fn current_commit(&self) -> CommitCounter {
if let Some(backdrop) = &self.probe { if let Some(backdrop) = &self.probe {
let _guard = backdrop.try_lock().expect("backdrop lock held during an element query"); let _guard = backdrop
.try_lock()
.expect("backdrop lock held during an element query");
} }
self.commit self.commit
} }
@@ -173,7 +181,11 @@ mod tests {
fn geometry(&self, _: RenderScale<f64>) -> Rectangle<i32, Physical> { fn geometry(&self, _: RenderScale<f64>) -> Rectangle<i32, Physical> {
self.rect self.rect
} }
fn damage_since(&self, _: RenderScale<f64>, commit: Option<CommitCounter>) -> DamageSet<i32, Physical> { fn damage_since(
&self,
_: RenderScale<f64>,
commit: Option<CommitCounter>,
) -> DamageSet<i32, Physical> {
if commit == Some(self.commit) { if commit == Some(self.commit) {
DamageSet::default() DamageSet::default()
} else { } else {
@@ -209,13 +221,31 @@ mod tests {
// Open: ignore a repeated instance of the cached popup material, and // Open: ignore a repeated instance of the cached popup material, and
// allow dependencies to query backdrop state without a nested lock. // allow dependencies to query backdrop state without a nested lock.
assert!(update_shared(&backdrop, &elements, &owner, region, 1.0.into())); assert!(update_shared(
&backdrop,
&elements,
&owner,
region,
1.0.into()
));
assert_eq!(backdrop.lock().unwrap().sources.len(), 1); assert_eq!(backdrop.lock().unwrap().sources.len(), 1);
assert!(!update_shared(&backdrop, &elements, &owner, region, 1.0.into())); assert!(!update_shared(
&backdrop,
&elements,
&owner,
region,
1.0.into()
));
// Update both content and bounds while the popup remains visible. // Update both content and bounds while the popup remains visible.
elements[1].repaint(vec![rect(10, 10, 5, 5)]); elements[1].repaint(vec![rect(10, 10, 5, 5)]);
assert!(update_shared(&backdrop, &elements, &owner, region, 1.0.into())); assert!(update_shared(
&backdrop,
&elements,
&owner,
region,
1.0.into()
));
assert!(update_shared( assert!(update_shared(
&backdrop, &backdrop,
&elements, &elements,
@@ -225,10 +255,28 @@ mod tests {
)); ));
// Close and reopen: dependencies disappear, then are tracked again. // Close and reopen: dependencies disappear, then are tracked again.
assert!(update_shared::<TestElement>(&backdrop, &[], &owner, region, 1.0.into())); assert!(update_shared::<TestElement>(
&backdrop,
&[],
&owner,
region,
1.0.into()
));
assert!(backdrop.lock().unwrap().sources.is_empty()); assert!(backdrop.lock().unwrap().sources.is_empty());
assert!(update_shared(&backdrop, &elements, &owner, region, 1.0.into())); assert!(update_shared(
assert!(!update_shared(&backdrop, &elements, &owner, region, 1.0.into())); &backdrop,
&elements,
&owner,
region,
1.0.into()
));
assert!(!update_shared(
&backdrop,
&elements,
&owner,
region,
1.0.into()
));
} }
#[test] #[test]
+35 -10
View File
@@ -33,10 +33,14 @@ fn render_window_texture(
if bytes == 0 || bytes > remaining { if bytes == 0 || bytes > remaining {
return Ok(None); return Ok(None);
} }
let content = render_elements_from_surface_tree::<GlesRenderer, WaylandSurfaceRenderElement<GlesRenderer>>( let content = render_elements_from_surface_tree::<
GlesRenderer,
WaylandSurfaceRenderElement<GlesRenderer>,
>(
renderer, renderer,
toplevel.wl_surface(), toplevel.wl_surface(),
Point::<i32, Logical>::from((-geometry.loc.x, -geometry.loc.y)).to_physical_precise_round(scale), Point::<i32, Logical>::from((-geometry.loc.x, -geometry.loc.y))
.to_physical_precise_round(scale),
scale, scale,
1.0, 1.0,
RenderElementKind::Unspecified, RenderElementKind::Unspecified,
@@ -46,7 +50,11 @@ fn render_window_texture(
} }
let mut texture = match reusable.filter(|texture| texture.size() == (size.w, size.h).into()) { let mut texture = match reusable.filter(|texture| texture.size() == (size.w, size.h).into()) {
Some(texture) => texture, Some(texture) => texture,
None => Offscreen::<GlesTexture>::create_buffer(renderer, Fourcc::Abgr8888, (size.w, size.h).into())?, None => Offscreen::<GlesTexture>::create_buffer(
renderer,
Fourcc::Abgr8888,
(size.w, size.h).into(),
)?,
}; };
{ {
let mut target = renderer.bind(&mut texture)?; let mut target = renderer.bind(&mut texture)?;
@@ -93,13 +101,22 @@ pub(crate) fn capture_window_frame(
.get_or_insert_threadsafe(|| Arc::new(CaptureTarget::default())) .get_or_insert_threadsafe(|| Arc::new(CaptureTarget::default()))
.clone(); .clone();
let reusable = cache.0.lock().unwrap().take(); let reusable = cache.0.lock().unwrap().take();
let mut snapshot = render_window_texture(renderer, window, geometry, scale, 128 * 1024 * 1024, reusable) let mut snapshot = render_window_texture(
.map_err(|error| error.to_string())? renderer,
.ok_or("Window has no capturable content")?; window,
geometry,
scale,
128 * 1024 * 1024,
reusable,
)
.map_err(|error| error.to_string())?
.ok_or("Window has no capturable content")?;
let size = snapshot.texture.size(); let size = snapshot.texture.size();
if let Some((state, cursor_geometry)) = cursor { if let Some((state, cursor_geometry)) = cursor {
let elements = cursor_elements(state, renderer, cursor_geometry, scale); let elements = cursor_elements(state, renderer, cursor_geometry, scale);
let mut target = renderer.bind(&mut snapshot.texture).map_err(|e| e.to_string())?; let mut target = renderer
.bind(&mut snapshot.texture)
.map_err(|e| e.to_string())?;
let mut frame = renderer let mut frame = renderer
.render(&mut target, (size.w, size.h).into(), Transform::Normal) .render(&mut target, (size.w, size.h).into(), Transform::Normal)
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
@@ -113,7 +130,11 @@ pub(crate) fn capture_window_frame(
let _ = frame.finish().map_err(|e| e.to_string())?; let _ = frame.finish().map_err(|e| e.to_string())?;
} }
let mapping = renderer let mapping = renderer
.copy_texture(&snapshot.texture, Rectangle::from_size(size), Fourcc::Abgr8888) .copy_texture(
&snapshot.texture,
Rectangle::from_size(size),
Fourcc::Abgr8888,
)
.map_err(|error| error.to_string())?; .map_err(|error| error.to_string())?;
if mapping.format() != Some(Fourcc::Abgr8888) { if mapping.format() != Some(Fourcc::Abgr8888) {
return Err("Unsupported window readback format".into()); return Err("Unsupported window readback format".into());
@@ -125,7 +146,9 @@ pub(crate) fn capture_window_frame(
// Smithay maps its PBO here and can wait for the GPU. Nonblocking readback // Smithay maps its PBO here and can wait for the GPU. Nonblocking readback
// needs a retained mapping plus an EGL-context-owned fence, polled before // needs a retained mapping plus an EGL-context-owned fence, polled before
// mapping; moving this call to the conversion worker is not context-safe. // mapping; moving this call to the conversion worker is not context-safe.
let source = renderer.map_texture(&mapping).map_err(|error| error.to_string())?; let source = renderer
.map_texture(&mapping)
.map_err(|error| error.to_string())?;
if source.len() < bytes { if source.len() < bytes {
return Err("Incomplete window readback".into()); return Err("Incomplete window readback".into());
} }
@@ -164,7 +187,9 @@ mod tests {
convert_window_pixels(&mut pixels); convert_window_pixels(&mut pixels);
assert_eq!( assert_eq!(
pixels, pixels,
[30, 20, 10, 255, 255, 128, 64, 128, 0, 0, 0, 0, 255, 255, 255, 1] [
30, 20, 10, 255, 255, 128, 64, 128, 0, 0, 0, 0, 255, 255, 255, 1
]
); );
} }
} }
+19 -6
View File
@@ -26,7 +26,11 @@ pub(crate) struct ClosedWindow {
impl ClosedWindow { impl ClosedWindow {
pub fn bytes(&self) -> usize { pub fn bytes(&self) -> usize {
self.snapshot.bytes() + self.handoff.as_ref().map_or(0, |(snapshot, _)| snapshot.bytes()) self.snapshot.bytes()
+ self
.handoff
.as_ref()
.map_or(0, |(snapshot, _)| snapshot.bytes())
} }
pub fn advance(&mut self, delta: Duration, spring: SpringConfig) -> bool { pub fn advance(&mut self, delta: Duration, spring: SpringConfig) -> bool {
@@ -82,7 +86,9 @@ pub(super) fn grouped_elements(
.render .render
.closing .closing
.iter() .iter()
.filter(|window| window.output == output_id && window.snapshot.context == renderer.context_id().erased()) .filter(|window| {
window.output == output_id && window.snapshot.context == renderer.context_id().erased()
})
.cloned() .cloned()
.collect(); .collect();
if windows.is_empty() { if windows.is_empty() {
@@ -97,7 +103,8 @@ pub(super) fn grouped_elements(
window.advance(delta, state.spring_config); window.advance(delta, state.spring_config);
} }
let visual = window.presentation.bounds.current; let visual = window.presentation.bounds.current;
let corners = RoundedRect::new(visual, output_geometry.loc, scale, window.radius).with_shape(window.shape); let corners = RoundedRect::new(visual, output_geometry.loc, scale, window.radius)
.with_shape(window.shape);
let constrain = rounded_visual_rect(visual, output_geometry.loc); let constrain = rounded_visual_rect(visual, output_geometry.loc);
let Some(programs) = corner_program(&mut state.render, renderer, window.shape) else { let Some(programs) = corner_program(&mut state.render, renderer, window.shape) else {
continue; continue;
@@ -124,7 +131,8 @@ pub(super) fn grouped_elements(
constrain, constrain,
corners, corners,
scale, scale,
theme.border_width + (theme.focus_ring_width - theme.border_width) * window.presentation.focus(), theme.border_width
+ (theme.focus_ring_width - theme.border_width) * window.presentation.focus(),
theme.border_color.0, theme.border_color.0,
theme.border_gradient, theme.border_gradient,
window.presentation.focus() as f32, window.presentation.focus() as f32,
@@ -196,8 +204,13 @@ pub(super) fn grouped_elements(
elements.push(material); elements.push(material);
} }
if window.material.is_none() if window.material.is_none()
&& let Some(blur) = && let Some(blur) = super::window_blur::window_blur_element(
super::window_blur::window_blur_element(state, renderer, output, window.presentation, corners) state,
renderer,
output,
window.presentation,
corners,
)
{ {
elements.push(blur); elements.push(blur);
} }
+113 -30
View File
@@ -219,7 +219,9 @@ fn window_corner_shader_timing_sample() {
Uniform::new("clip_rect", [0.0f32, 0.0, 1024.0, 768.0]), Uniform::new("clip_rect", [0.0f32, 0.0, 1024.0, 768.0]),
Uniform::new("radius", 16.0f32), Uniform::new("radius", 16.0f32),
]; ];
let mut texture: GlesTexture = renderer.create_buffer(Fourcc::Abgr8888, (1024, 768).into()).unwrap(); let mut texture: GlesTexture = renderer
.create_buffer(Fourcc::Abgr8888, (1024, 768).into())
.unwrap();
for shape in [CornerShape::Circular, CornerShape::Continuous] { for shape in [CornerShape::Circular, CornerShape::Continuous] {
let programs = corner_program(&mut resources, &mut renderer, shape).unwrap(); let programs = corner_program(&mut resources, &mut renderer, shape).unwrap();
@@ -228,7 +230,9 @@ fn window_corner_shader_timing_sample() {
for iteration in 0..110 { for iteration in 0..110 {
let started = std::time::Instant::now(); let started = std::time::Instant::now();
let mut target = renderer.bind(&mut texture).unwrap(); let mut target = renderer.bind(&mut texture).unwrap();
let mut frame = renderer.render(&mut target, size, Transform::Normal).unwrap(); let mut frame = renderer
.render(&mut target, size, Transform::Normal)
.unwrap();
frame.clear(Color32F::TRANSPARENT, &[rect]).unwrap(); frame.clear(Color32F::TRANSPARENT, &[rect]).unwrap();
frame frame
.render_pixel_shader_to( .render_pixel_shader_to(
@@ -256,7 +260,11 @@ fn window_corner_shader_timing_sample() {
} }
} }
fn rasterize(renderer: &mut GlesRenderer, program: &GlesPixelProgram, uniforms: &[Uniform<'_>]) -> Vec<u8> { fn rasterize(
renderer: &mut GlesRenderer,
program: &GlesPixelProgram,
uniforms: &[Uniform<'_>],
) -> Vec<u8> {
let size = (64, 64).into(); let size = (64, 64).into();
let mut texture: GlesTexture = renderer.create_buffer(Fourcc::Abgr8888, size).unwrap(); let mut texture: GlesTexture = renderer.create_buffer(Fourcc::Abgr8888, size).unwrap();
let rect = Rectangle::<i32, Physical>::from_size((64, 64).into()); let rect = Rectangle::<i32, Physical>::from_size((64, 64).into());
@@ -294,7 +302,8 @@ fn panel_shader_matches_core_outlines_and_resets_for_popovers() {
use smithay::backend::renderer::gles::UniformValue; use smithay::backend::renderer::gles::UniformValue;
let mut renderer = renderer(); let mut renderer = renderer();
let mut resources = RenderResources::default(); let mut resources = RenderResources::default();
let program = material_program_for_corners(&mut resources, &mut renderer, CornerShape::Circular).unwrap(); let program =
material_program_for_corners(&mut resources, &mut renderer, CornerShape::Circular).unwrap();
let bounds = ferese_layout::Rect::new(0., 0., 64., 64.); let bounds = ferese_layout::Rect::new(0., 0., 64., 64.);
let groups = [ let groups = [
GroupBounds { GroupBounds {
@@ -346,7 +355,19 @@ fn panel_shader_matches_core_outlines_and_resets_for_popovers() {
let matrices = scene let matrices = scene
.packets() .packets()
.into_iter() .into_iter()
.map(|p| [p[0], p[1], p[2], p[3], p[4], p[5], p[6], p[7], p[8] * 4. + p[9]]) .map(|p| {
[
p[0],
p[1],
p[2],
p[3],
p[4],
p[5],
p[6],
p[7],
p[8] * 4. + p[9],
]
})
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let mut uniforms = vec![ let mut uniforms = vec![
Uniform::new("visible_rect", [0.0f32, 0., 64., 64.]), Uniform::new("visible_rect", [0.0f32, 0., 64., 64.]),
@@ -441,7 +462,10 @@ fn apple_window_fill_and_inset_border_match_path_reference_at_fractional_scales(
let inner = coverage(distance + f64::from(width)); let inner = coverage(distance + f64::from(width));
let index = (y * 64 + x) * 4; let index = (y * 64 + x) * 4;
for (actual, expected) in [(fill[index + 3], outer), (edge[index + 3], (outer - inner).max(0.0))] { for (actual, expected) in [
(fill[index + 3], outer),
(edge[index + 3], (outer - inner).max(0.0)),
] {
let expected = (expected * 255.0).round() as u8; let expected = (expected * 255.0).round() as u8;
assert!( assert!(
actual.abs_diff(expected) <= 2, actual.abs_diff(expected) <= 2,
@@ -510,9 +534,12 @@ fn apple_shoulder_extent_never_claims_transparent_pixels_as_opaque() {
fn docked_panel_fill_covers_the_entire_attached_edge() { fn docked_panel_fill_covers_the_entire_attached_edge() {
let mut renderer = renderer(); let mut renderer = renderer();
let mut resources = RenderResources::default(); let mut resources = RenderResources::default();
let material = material_program_for_corners(&mut resources, &mut renderer, CornerShape::Circular).unwrap(); let material =
material_program_for_corners(&mut resources, &mut renderer, CornerShape::Circular).unwrap();
for edge in [ferese_panel::Edge::Top, ferese_panel::Edge::Bottom] { for edge in [ferese_panel::Edge::Top, ferese_panel::Edge::Bottom] {
let radii = ferese_panel::CornerRadii([14., 8., 12., 20.]).at_edge(edge, true).0; let radii = ferese_panel::CornerRadii([14., 8., 12., 20.])
.at_edge(edge, true)
.0;
let pixels = rasterize( let pixels = rasterize(
&mut renderer, &mut renderer,
&material.0, &material.0,
@@ -537,7 +564,11 @@ fn docked_panel_fill_covers_the_entire_attached_edge() {
"wallpaper exposed at {edge:?} pixel {x}" "wallpaper exposed at {edge:?} pixel {x}"
); );
} }
assert_eq!(pixels[free_row * 64 * 4 + 3], 0, "opposite corners remain rounded"); assert_eq!(
pixels[free_row * 64 * 4 + 3],
0,
"opposite corners remain rounded"
);
} }
} }
@@ -546,7 +577,8 @@ fn docked_panel_fill_covers_the_entire_attached_edge() {
fn asymmetric_panel_fill_and_shadow_match_each_corner() { fn asymmetric_panel_fill_and_shadow_match_each_corner() {
let mut renderer = renderer(); let mut renderer = renderer();
let mut resources = RenderResources::default(); let mut resources = RenderResources::default();
let material = material_program_for_corners(&mut resources, &mut renderer, CornerShape::Circular).unwrap(); let material =
material_program_for_corners(&mut resources, &mut renderer, CornerShape::Circular).unwrap();
let rect = [2.0f32, 2.0, 60.0, 60.0]; let rect = [2.0f32, 2.0, 60.0, 60.0];
let radii = [0.0f32, 4.0, 12.0, 20.0]; let radii = [0.0f32, 4.0, 12.0, 20.0];
for paint_mode in [0.0f32, 2.0] { for paint_mode in [0.0f32, 2.0] {
@@ -596,7 +628,9 @@ fn apple_shadow_and_window_tint_match_the_same_outline() {
let mut renderer = renderer(); let mut renderer = renderer();
let mut resources = RenderResources::default(); let mut resources = RenderResources::default();
let programs = corner_program(&mut resources, &mut renderer, CornerShape::Continuous).unwrap(); let programs = corner_program(&mut resources, &mut renderer, CornerShape::Continuous).unwrap();
let material = material_program_for_corners(&mut resources, &mut renderer, CornerShape::Continuous).unwrap(); let material =
material_program_for_corners(&mut resources, &mut renderer, CornerShape::Continuous)
.unwrap();
let rect = [1.25f32, 2.5, 61.5, 59.0]; let rect = [1.25f32, 2.5, 61.5, 59.0];
let radius = 12.375f32; let radius = 12.375f32;
let shadow = rasterize( let shadow = rasterize(
@@ -634,7 +668,10 @@ fn apple_shadow_and_window_tint_match_the_same_outline() {
let shadow_alpha = (-0.5 * (distance.max(0.0) / 4.0).powi(2)).exp(); let shadow_alpha = (-0.5 * (distance.max(0.0) / 4.0).powi(2)).exp();
let index = (y * 64 + x) * 4; let index = (y * 64 + x) * 4;
for (actual, expected) in [(tint[index + 3], coverage(distance)), (shadow[index + 3], shadow_alpha)] { for (actual, expected) in [
(tint[index + 3], coverage(distance)),
(shadow[index + 3], shadow_alpha),
] {
assert!( assert!(
actual.abs_diff((expected * 255.0).round() as u8) <= 2, actual.abs_diff((expected * 255.0).round() as u8) <= 2,
"outline mismatch at ({x}, {y})" "outline mismatch at ({x}, {y})"
@@ -680,8 +717,16 @@ fn window_role_change_invalidates_decorations_and_preserves_the_resize_snapshot(
.unwrap() .unwrap()
.borders .borders
.contexts .contexts
.insert(context.clone(), CachedBorder { element, parameters }); .insert(
let texture = renderer.create_buffer(Fourcc::Abgr8888, (64, 64).into()).unwrap(); context.clone(),
CachedBorder {
element,
parameters,
},
);
let texture = renderer
.create_buffer(Fourcc::Abgr8888, (64, 64).into())
.unwrap();
resources.set_snapshot( resources.set_snapshot(
id, id,
ResizeSnapshot { ResizeSnapshot {
@@ -711,7 +756,10 @@ fn window_role_change_invalidates_decorations_and_preserves_the_resize_snapshot(
fn endpoint_derivatives(points: Cubic, end: bool) -> (Point2, Point2) { fn endpoint_derivatives(points: Cubic, end: bool) -> (Point2, Point2) {
if end { if end {
( (
[3.0 * (points[3][0] - points[2][0]), 3.0 * (points[3][1] - points[2][1])], [
3.0 * (points[3][0] - points[2][0]),
3.0 * (points[3][1] - points[2][1]),
],
[ [
6.0 * (points[3][0] - 2.0 * points[2][0] + points[1][0]), 6.0 * (points[3][0] - 2.0 * points[2][0] + points[1][0]),
6.0 * (points[3][1] - 2.0 * points[2][1] + points[1][1]), 6.0 * (points[3][1] - 2.0 * points[2][1] + points[1][1]),
@@ -719,7 +767,10 @@ fn endpoint_derivatives(points: Cubic, end: bool) -> (Point2, Point2) {
) )
} else { } else {
( (
[3.0 * (points[1][0] - points[0][0]), 3.0 * (points[1][1] - points[0][1])], [
3.0 * (points[1][0] - points[0][0]),
3.0 * (points[1][1] - points[0][1]),
],
[ [
6.0 * (points[2][0] - 2.0 * points[1][0] + points[0][0]), 6.0 * (points[2][0] - 2.0 * points[1][0] + points[0][0]),
6.0 * (points[2][1] - 2.0 * points[1][1] + points[0][1]), 6.0 * (points[2][1] - 2.0 * points[1][1] + points[0][1]),
@@ -729,7 +780,8 @@ fn endpoint_derivatives(points: Cubic, end: bool) -> (Point2, Point2) {
} }
fn curvature(tangent: Point2, acceleration: Point2) -> f64 { fn curvature(tangent: Point2, acceleration: Point2) -> f64 {
(tangent[0] * acceleration[1] - tangent[1] * acceleration[0]).abs() / tangent[0].hypot(tangent[1]).powi(3) (tangent[0] * acceleration[1] - tangent[1] * acceleration[0]).abs()
/ tangent[0].hypot(tangent[1]).powi(3)
} }
#[test] #[test]
@@ -745,10 +797,12 @@ fn measured_uikit_profile_has_flat_shoulders_but_small_internal_join_discontinui
assert_eq!(pair[0][3], pair[1][0], "join must be position-continuous"); assert_eq!(pair[0][3], pair[1][0], "join must be position-continuous");
let (before, before_acceleration) = endpoint_derivatives(pair[0], true); let (before, before_acceleration) = endpoint_derivatives(pair[0], true);
let (after, after_acceleration) = endpoint_derivatives(pair[1], false); let (after, after_acceleration) = endpoint_derivatives(pair[1], false);
let cosine = let cosine = (before[0] * after[0] + before[1] * after[1])
(before[0] * after[0] + before[1] * after[1]) / before[0].hypot(before[1]) / after[0].hypot(after[1]); / before[0].hypot(before[1])
/ after[0].hypot(after[1]);
let angle = cosine.clamp(-1.0, 1.0).acos().to_degrees(); let angle = cosine.clamp(-1.0, 1.0).acos().to_degrees();
let curvature_jump = (curvature(before, before_acceleration) - curvature(after, after_acceleration)).abs(); let curvature_jump =
(curvature(before, before_acceleration) - curvature(after, after_acceleration)).abs();
// This is a measured Apple-style approximation, not a globally G1/G2 // This is a measured Apple-style approximation, not a globally G1/G2
// curve. Preserve and document its limitation instead of falsely // curve. Preserve and document its limitation instead of falsely
// passing a continuity test, or silently changing the source profile. // passing a continuity test, or silently changing the source profile.
@@ -767,7 +821,9 @@ fn corner_heavy_shader_timing_sample() {
let current = include_str!("shaders/window_corners.glsl"); let current = include_str!("shaders/window_corners.glsl");
let size = (1024, 768).into(); let size = (1024, 768).into();
let rect = Rectangle::<i32, Physical>::from_size(size); let rect = Rectangle::<i32, Physical>::from_size(size);
let mut texture: GlesTexture = renderer.create_buffer(Fourcc::Abgr8888, (1024, 768).into()).unwrap(); let mut texture: GlesTexture = renderer
.create_buffer(Fourcc::Abgr8888, (1024, 768).into())
.unwrap();
let body = "\nvoid main() { float d = rounded_rect_distance(mod(gl_FragCoord.xy, vec2(64.0)), vec4(0.0, 0.0, 64.0, 64.0), radius); gl_FragColor = vec4(edge_coverage(d)); }"; let body = "\nvoid main() { float d = rounded_rect_distance(mod(gl_FragCoord.xy, vec2(64.0)), vec4(0.0, 0.0, 64.0, 64.0), radius); gl_FragColor = vec4(edge_coverage(d)); }";
for (name, source) in reference for (name, source) in reference
.as_deref() .as_deref()
@@ -784,7 +840,9 @@ fn corner_heavy_shader_timing_sample() {
for i in 0..110 { for i in 0..110 {
let start = std::time::Instant::now(); let start = std::time::Instant::now();
let mut target = renderer.bind(&mut texture).unwrap(); let mut target = renderer.bind(&mut texture).unwrap();
let mut frame = renderer.render(&mut target, size, Transform::Normal).unwrap(); let mut frame = renderer
.render(&mut target, size, Transform::Normal)
.unwrap();
frame.clear(Color32F::TRANSPARENT, &[rect]).unwrap(); frame.clear(Color32F::TRANSPARENT, &[rect]).unwrap();
frame frame
.render_pixel_shader_to( .render_pixel_shader_to(
@@ -817,7 +875,9 @@ fn window_tint_does_not_inherit_panel_geometry() {
use smithay::backend::renderer::gles::UniformValue; use smithay::backend::renderer::gles::UniformValue;
use smithay::output::{Mode, PhysicalProperties, Subpixel}; use smithay::output::{Mode, PhysicalProperties, Subpixel};
fn paint(renderer: &mut GlesRenderer, element: &impl RenderElement<GlesRenderer>) -> Vec<u8> { fn paint(renderer: &mut GlesRenderer, element: &impl RenderElement<GlesRenderer>) -> Vec<u8> {
let mut texture: GlesTexture = renderer.create_buffer(Fourcc::Abgr8888, (96, 96).into()).unwrap(); let mut texture: GlesTexture = renderer
.create_buffer(Fourcc::Abgr8888, (96, 96).into())
.unwrap();
let mut target = renderer.bind(&mut texture).unwrap(); let mut target = renderer.bind(&mut texture).unwrap();
let rect = Rectangle::from_size((96, 96).into()); let rect = Rectangle::from_size((96, 96).into());
let mut frame = renderer let mut frame = renderer
@@ -837,7 +897,11 @@ fn window_tint_does_not_inherit_panel_geometry() {
.unwrap(); .unwrap();
let _ = frame.finish().unwrap(); let _ = frame.finish().unwrap();
let mapping = renderer let mapping = renderer
.copy_framebuffer(&target, Rectangle::from_size((96, 96).into()), Fourcc::Abgr8888) .copy_framebuffer(
&target,
Rectangle::from_size((96, 96).into()),
Fourcc::Abgr8888,
)
.unwrap(); .unwrap();
renderer.map_texture(&mapping).unwrap().to_vec() renderer.map_texture(&mapping).unwrap().to_vec()
} }
@@ -863,7 +927,8 @@ fn window_tint_does_not_inherit_panel_geometry() {
Some((0, 0).into()), Some((0, 0).into()),
); );
let geometry = Rectangle::new((6, 6).into(), (80, 80).into()); let geometry = Rectangle::new((6, 6).into(), (80, 80).into());
let corners = RoundedRect::from_logical(geometry, 1.0, 12.0).with_shape(CornerShape::Continuous); let corners =
RoundedRect::from_logical(geometry, 1.0, 12.0).with_shape(CornerShape::Continuous);
for resize_fill in [false, true] { for resize_fill in [false, true] {
let tint = window_tint_element( let tint = window_tint_element(
&mut resources, &mut resources,
@@ -949,14 +1014,23 @@ fn floating_blur_sampling_halo_does_not_paint_neighboring_islands() {
]; ];
let rect = Rectangle::<i32, Physical>::new( let rect = Rectangle::<i32, Physical>::new(
(capture[0] as i32, capture[1] as i32).into(), (capture[0] as i32, capture[1] as i32).into(),
((capture[2] - capture[0]) as i32, (capture[3] - capture[1]) as i32).into(), (
(capture[2] - capture[0]) as i32,
(capture[3] - capture[1]) as i32,
)
.into(),
); );
let mut pixels = vec![128_u8; (rect.size.w * rect.size.h * 4) as usize]; let mut pixels = vec![128_u8; (rect.size.w * rect.size.h * 4) as usize];
for p in pixels.chunks_exact_mut(4) { for p in pixels.chunks_exact_mut(4) {
p[3] = 255; p[3] = 255;
} }
let source = renderer let source = renderer
.import_memory(&pixels, Fourcc::Abgr8888, (rect.size.w, rect.size.h).into(), false) .import_memory(
&pixels,
Fourcc::Abgr8888,
(rect.size.w, rect.size.h).into(),
false,
)
.unwrap(); .unwrap();
let uniforms = vec![ let uniforms = vec![
Uniform::new("visible_rect", visible), Uniform::new("visible_rect", visible),
@@ -981,10 +1055,15 @@ fn floating_blur_sampling_halo_does_not_paint_neighboring_islands() {
.create_buffer(Fourcc::Abgr8888, (size.w, size.h).into()) .create_buffer(Fourcc::Abgr8888, (size.w, size.h).into())
.unwrap(); .unwrap();
let mut target = renderer.bind(&mut texture).unwrap(); let mut target = renderer.bind(&mut texture).unwrap();
let mut frame = renderer.render(&mut target, size, Transform::Normal).unwrap(); let mut frame = renderer
.render(&mut target, size, Transform::Normal)
.unwrap();
frame.clear([0.; 4].into(), &[full]).unwrap(); frame.clear([0.; 4].into(), &[full]).unwrap();
// OpenGL coordinates have the opposite Y direction from the frame. // OpenGL coordinates have the opposite Y direction from the frame.
let dst = Rectangle::new((rect.loc.x, size.h - rect.loc.y - rect.size.h).into(), rect.size); let dst = Rectangle::new(
(rect.loc.x, size.h - rect.loc.y - rect.size.h).into(),
rect.size,
);
frame frame
.render_texture_from_to( .render_texture_from_to(
&source, &source,
@@ -1000,7 +1079,11 @@ fn floating_blur_sampling_halo_does_not_paint_neighboring_islands() {
.unwrap(); .unwrap();
let _ = frame.finish().unwrap(); let _ = frame.finish().unwrap();
let mapping = renderer let mapping = renderer
.copy_framebuffer(&target, Rectangle::from_size((size.w, size.h).into()), Fourcc::Abgr8888) .copy_framebuffer(
&target,
Rectangle::from_size((size.w, size.h).into()),
Fourcc::Abgr8888,
)
.unwrap(); .unwrap();
let rendered = renderer.map_texture(&mapping).unwrap().to_vec(); let rendered = renderer.map_texture(&mapping).unwrap().to_vec();
let mut painted = 0; let mut painted = 0;
+86 -32
View File
@@ -13,10 +13,14 @@ pub(super) fn corner_program(
shape: CornerShape, shape: CornerShape,
) -> Option<RoundedClipPrograms> { ) -> Option<RoundedClipPrograms> {
let context = renderer.context_id().erased(); let context = renderer.context_id().erased();
if let Some(program) = resources.contexts.get(&context).and_then(|programs| match shape { if let Some(program) = resources
CornerShape::Circular => programs.rounded.as_ref(), .contexts
CornerShape::Continuous => programs.continuous.as_ref(), .get(&context)
}) { .and_then(|programs| match shape {
CornerShape::Circular => programs.rounded.as_ref(),
CornerShape::Continuous => programs.continuous.as_ref(),
})
{
return Some(program.clone()); return Some(program.clone());
} }
@@ -43,11 +47,18 @@ pub(super) fn corner_program(
UniformName::new("opacity", UniformType::_1f), UniformName::new("opacity", UniformType::_1f),
UniformName::new("shadow_color", UniformType::_4f), UniformName::new("shadow_color", UniformType::_4f),
]; ];
let texture = let texture = renderer.compile_custom_texture_shader(
renderer.compile_custom_texture_shader(corner_shader_for(ROUNDED_TEXTURE_SHADER, shape), &texture_uniforms); corner_shader_for(ROUNDED_TEXTURE_SHADER, shape),
let border = &texture_uniforms,
renderer.compile_custom_pixel_shader(corner_shader_for(ROUNDED_BORDER_SHADER, shape), &border_uniforms); );
let shadow = renderer.compile_custom_pixel_shader(corner_shader_for(WINDOW_SHADOW_SHADER, shape), &shadow_uniforms); let border = renderer.compile_custom_pixel_shader(
corner_shader_for(ROUNDED_BORDER_SHADER, shape),
&border_uniforms,
);
let shadow = renderer.compile_custom_pixel_shader(
corner_shader_for(WINDOW_SHADOW_SHADER, shape),
&shadow_uniforms,
);
let solid = renderer.compile_custom_pixel_shader( let solid = renderer.compile_custom_pixel_shader(
corner_shader_for(ROUNDED_SOLID_SHADER, shape), corner_shader_for(ROUNDED_SOLID_SHADER, shape),
&[ &[
@@ -57,7 +68,9 @@ pub(super) fn corner_program(
], ],
); );
let compiled = texture.and_then(|texture| { let compiled = texture.and_then(|texture| {
border.and_then(|border| shadow.and_then(|shadow| solid.map(|solid| (texture, border, shadow, solid)))) border.and_then(|border| {
shadow.and_then(|shadow| solid.map(|solid| (texture, border, shadow, solid)))
})
}); });
match compiled { match compiled {
Ok((texture, border, shadow, solid)) => { Ok((texture, border, shadow, solid)) => {
@@ -130,13 +143,22 @@ pub(super) fn window_border_element(
Some(g) => ( Some(g) => (
g.from.0, g.from.0,
g.to.0, g.to.0,
border_gradient_line(corners.rect, mode.size, output.current_transform().invert(), g.angle), border_gradient_line(
corners.rect,
mode.size,
output.current_transform().invert(),
g.angle,
),
), ),
None => (theme.accent_color.0, theme.accent_color.0, [0.0; 4]), None => (theme.accent_color.0, theme.accent_color.0, [0.0; 4]),
}; };
let parameters = BorderParameters { let parameters = BorderParameters {
geometry, geometry,
clip_rect: framebuffer_clip_rect(corners.rect, mode.size, output.current_transform().invert()), clip_rect: framebuffer_clip_rect(
corners.rect,
mode.size,
output.current_transform().invert(),
),
radius: corners.radius, radius: corners.radius,
width, width,
color: color_with_alpha(from, opacity), color: color_with_alpha(from, opacity),
@@ -235,7 +257,11 @@ pub(super) fn window_tint_element(
let program = material_program_for_corners(resources, renderer, corners.shape)?; let program = material_program_for_corners(resources, renderer, corners.shape)?;
let parameters = BorderParameters { let parameters = BorderParameters {
geometry, geometry,
clip_rect: framebuffer_clip_rect(corners.rect, mode.size, output.current_transform().invert()), clip_rect: framebuffer_clip_rect(
corners.rect,
mode.size,
output.current_transform().invert(),
),
radius: corners.radius, radius: corners.radius,
width: 0.0, width: 0.0,
color, color,
@@ -253,17 +279,20 @@ pub(super) fn window_tint_element(
} else { } else {
&mut resources.windows.entry(id).or_default().dim &mut resources.windows.entry(id).or_default().dim
}; };
let cached = buffers.contexts.entry(context).or_insert_with(|| CachedBorder { let cached = buffers
element: SharedPixelShaderElement::new( .contexts
program.0, .entry(context)
geometry, .or_insert_with(|| CachedBorder {
None, element: SharedPixelShaderElement::new(
1.0, program.0,
uniforms(&parameters), geometry,
RenderElementKind::Unspecified, None,
), 1.0,
parameters: parameters.clone(), uniforms(&parameters),
}); RenderElementKind::Unspecified,
),
parameters: parameters.clone(),
});
if cached.parameters != parameters { if cached.parameters != parameters {
if cached.parameters.geometry != geometry { if cached.parameters.geometry != geometry {
cached.element.resize(geometry, None); cached.element.resize(geometry, None);
@@ -311,7 +340,11 @@ pub(super) fn window_shadow_element(
let parameters = ShadowParameters { let parameters = ShadowParameters {
blur: (blur * scale) as f32, blur: (blur * scale) as f32,
bounds, bounds,
shadow_rect: framebuffer_clip_rect(shadow_geometry, mode.size, output.current_transform().invert()), shadow_rect: framebuffer_clip_rect(
shadow_geometry,
mode.size,
output.current_transform().invert(),
),
radius: corners.radius, radius: corners.radius,
opacity: opacity as f32, opacity: opacity as f32,
color, color,
@@ -349,8 +382,16 @@ pub(super) fn window_shadow_element(
Some(PhysicalShaderElement { Some(PhysicalShaderElement {
inner: cached.element.clone(), inner: cached.element.clone(),
geometry: Rectangle::new( geometry: Rectangle::new(
(shadow_geometry.loc.x - extent, shadow_geometry.loc.y - extent).into(), (
(shadow_geometry.size.w + 2 * extent, shadow_geometry.size.h + 2 * extent).into(), shadow_geometry.loc.x - extent,
shadow_geometry.loc.y - extent,
)
.into(),
(
shadow_geometry.size.w + 2 * extent,
shadow_geometry.size.h + 2 * extent,
)
.into(),
), ),
}) })
} }
@@ -365,7 +406,11 @@ pub(super) fn shadow_uniforms(parameters: &ShadowParameters) -> Vec<Uniform<'sta
] ]
} }
pub(super) fn shadow_bounds(geometry: Rectangle<i32, Logical>, offset_y: f64, blur: f64) -> Rectangle<i32, Logical> { pub(super) fn shadow_bounds(
geometry: Rectangle<i32, Logical>,
offset_y: f64,
blur: f64,
) -> Rectangle<i32, Logical> {
let extent = (blur * 2.0).ceil() as i32; let extent = (blur * 2.0).ceil() as i32;
let offset_y = offset_y.round() as i32; let offset_y = offset_y.round() as i32;
@@ -399,21 +444,30 @@ pub(super) fn rounded_window_elements(
let geometry = window.geometry(); let geometry = window.geometry();
let mut reference = geometry.to_physical_precise_round(scale); let mut reference = geometry.to_physical_precise_round(scale);
let behavior = if let Some(size) = native_size { let behavior = if let Some(size) = native_size {
reference.size = Size::<i32, Logical>::from((size.width, size.height)).to_physical_precise_round(scale); reference.size =
Size::<i32, Logical>::from((size.width, size.height)).to_physical_precise_round(scale);
ConstrainScaleBehavior::Stretch ConstrainScaleBehavior::Stretch
} else { } else {
behavior behavior
}; };
let location = physical_constrain.loc - geometry.loc.to_physical_precise_round(scale); let location = physical_constrain.loc - geometry.loc.to_physical_precise_round(scale);
let clip = framebuffer_clip_rect(physical_constrain, mode.size, output.current_transform().invert()); let clip = framebuffer_clip_rect(
physical_constrain,
mode.size,
output.current_transform().invert(),
);
let radius = corners.radius; let radius = corners.radius;
let surface = toplevel.wl_surface(); let surface = toplevel.wl_surface();
let mut content = PopupManager::popups_for_surface(surface) let mut content = PopupManager::popups_for_surface(surface)
.flat_map(|(popup, popup_offset)| { .flat_map(|(popup, popup_offset)| {
let offset = (geometry.loc + popup_offset - popup.geometry().loc).to_physical_precise_round(scale); let offset = (geometry.loc + popup_offset - popup.geometry().loc)
.to_physical_precise_round(scale);
render_elements_from_surface_tree::<GlesRenderer, WaylandSurfaceRenderElement<GlesRenderer>>( render_elements_from_surface_tree::<
GlesRenderer,
WaylandSurfaceRenderElement<GlesRenderer>,
>(
renderer, renderer,
popup.wl_surface(), popup.wl_surface(),
location + offset, location + offset,
+169 -63
View File
@@ -12,27 +12,34 @@ pub(super) fn append_material_surface(
output_crop: Rectangle<i32, Physical>, output_crop: Rectangle<i32, Physical>,
elements: &mut Vec<AnimatedWindowRenderElement>, elements: &mut Vec<AnimatedWindowRenderElement>,
) { ) {
let radius = let radius = if crate::effects::surface_role(surface)
if crate::effects::surface_role(surface).is_some_and(|(role, _)| role == crate::effects::SemanticRole::Panel) { .is_some_and(|(role, _)| role == crate::effects::SemanticRole::Panel)
state.theme_settings.panel_radius {
} else { state.theme_settings.panel_radius
state.theme_settings.material_radius } else {
}; state.theme_settings.material_radius
};
let regions = crate::effects::surface_regions(surface); let regions = crate::effects::surface_regions(surface);
let opacities = crate::effects::surface_region_opacities(surface); let opacities = crate::effects::surface_region_opacities(surface);
let targets: Vec<_> = match &regions { let targets: Vec<_> = match &regions {
None => vec![(geometry, RoundedRect::from_logical(geometry, scale, radius), 1.0)], None => vec![(
geometry,
RoundedRect::from_logical(geometry, scale, radius),
1.0,
)],
Some(regions) => regions Some(regions) => regions
.iter() .iter()
.enumerate() .enumerate()
.filter_map(|(index, r)| { .filter_map(|(index, r)| {
material_region_geometry(*r, geometry, content_origin, scale).map(|(rect, corners)| { material_region_geometry(*r, geometry, content_origin, scale).map(
( |(rect, corners)| {
rect, (
corners, rect,
opacities.get(index).copied().unwrap_or(1000) as f32 / 1000.0, corners,
) opacities.get(index).copied().unwrap_or(1000) as f32 / 1000.0,
}) )
},
)
}) })
.collect(), .collect(),
}; };
@@ -63,8 +70,12 @@ pub(super) fn append_material_surface(
}) })
.collect(); .collect();
if let Some(buffers) = state.render.surfaces.get_mut(surface) { if let Some(buffers) = state.render.surfaces.get_mut(surface) {
buffers.contexts.retain(|(_, _, index), _| *index < targets.len()); buffers
buffers.captures.retain(|(_, _, index), _| *index < targets.len()); .contexts
.retain(|(_, _, index), _| *index < targets.len());
buffers
.captures
.retain(|(_, _, index), _| *index < targets.len());
} }
let material = (!materials.is_empty() && regions.is_none()).then_some(()); let material = (!materials.is_empty() && regions.is_none()).then_some(());
let clip = material.as_ref().and_then(|_| { let clip = material.as_ref().and_then(|_| {
@@ -78,7 +89,10 @@ pub(super) fn append_material_surface(
)) ))
}); });
// Smithay element lists are front to back: content, material, shadow. // Smithay element lists are front to back: content, material, shadow.
let content = render_elements_from_surface_tree::<GlesRenderer, WaylandSurfaceRenderElement<GlesRenderer>>( let content = render_elements_from_surface_tree::<
GlesRenderer,
WaylandSurfaceRenderElement<GlesRenderer>,
>(
renderer, renderer,
surface, surface,
content_origin.to_physical_precise_round(scale), content_origin.to_physical_precise_round(scale),
@@ -118,7 +132,11 @@ fn material_region_geometry(
scale: f64, scale: f64,
) -> Option<(Rectangle<i32, Logical>, RoundedRect)> { ) -> Option<(Rectangle<i32, Logical>, RoundedRect)> {
let rect = Rectangle::<f64, Logical>::new( let rect = Rectangle::<f64, Logical>::new(
(f64::from(origin.x) + region[0], f64::from(origin.y) + region[1]).into(), (
f64::from(origin.x) + region[0],
f64::from(origin.y) + region[1],
)
.into(),
(region[2], region[3]).into(), (region[2], region[3]).into(),
) )
.intersection(geometry.to_f64())?; .intersection(geometry.to_f64())?;
@@ -135,7 +153,10 @@ fn material_region_geometry(
fn framebuffer_corner_radii(radii: [f32; 4], transform: Transform) -> [f32; 4] { fn framebuffer_corner_radii(radii: [f32; 4], transform: Transform) -> [f32; 4] {
let size = Size::<f64, Physical>::from((1., 1.)); let size = Size::<f64, Physical>::from((1., 1.));
let mut transformed = [0.; 4]; let mut transformed = [0.; 4];
for (radius, point) in radii.into_iter().zip([(0., 0.), (1., 0.), (1., 1.), (0., 1.)]) { for (radius, point) in radii
.into_iter()
.zip([(0., 0.), (1., 0.), (1., 1.), (0., 1.)])
{
let point = transform.transform_point_in(Point::<f64, Physical>::from(point), &size); let point = transform.transform_point_in(Point::<f64, Physical>::from(point), &size);
let index = match (point.x > 0.5, point.y > 0.5) { let index = match (point.x > 0.5, point.y > 0.5) {
(false, false) => 0, (false, false) => 0,
@@ -181,7 +202,11 @@ fn resolve_surface_material(
panel_opacity: Option<f32>, panel_opacity: Option<f32>,
panel_style: ferese_panel::PanelStyle, panel_style: ferese_panel::PanelStyle,
) -> crate::effects::ResolvedMaterial { ) -> crate::effects::ResolvedMaterial {
let mut material = crate::effects::resolve_material(role, settings.material_style, settings.shell_opacity as f32); let mut material = crate::effects::resolve_material(
role,
settings.material_style,
settings.shell_opacity as f32,
);
if role == crate::effects::SemanticRole::Panel { if role == crate::effects::SemanticRole::Panel {
material.opacity = ferese_config::theme::effective_panel_opacity( material.opacity = ferese_config::theme::effective_panel_opacity(
material.opacity, material.opacity,
@@ -226,7 +251,8 @@ pub(super) fn material_element_with_role(
} else { } else {
state.theme_settings.surface_base_color state.theme_settings.surface_base_color
}; };
let morph = role == crate::effects::SemanticRole::Panel && state.panel_style == ferese_panel::PanelStyle::Morph; let morph = role == crate::effects::SemanticRole::Panel
&& state.panel_style == ferese_panel::PanelStyle::Morph;
let background = if morph { let background = if morph {
state.theme_settings.surface_raised_color state.theme_settings.surface_raised_color
} else { } else {
@@ -250,8 +276,8 @@ pub(super) fn material_element_with_role(
.to_logical(scale) .to_logical(scale)
.to_i32_ceil(); .to_i32_ceil();
let touches_top = role == crate::effects::SemanticRole::Panel && geometry.loc.y == 0; let touches_top = role == crate::effects::SemanticRole::Panel && geometry.loc.y == 0;
let touches_bottom = let touches_bottom = role == crate::effects::SemanticRole::Panel
role == crate::effects::SemanticRole::Panel && geometry.loc.y + geometry.size.h >= output_size.h; && geometry.loc.y + geometry.size.h >= output_size.h;
let radii = ferese_panel::CornerRadii(radii) let radii = ferese_panel::CornerRadii(radii)
.at_top_edge(touches_top) .at_top_edge(touches_top)
.at_edge(ferese_panel::Edge::Bottom, touches_bottom) .at_edge(ferese_panel::Edge::Bottom, touches_bottom)
@@ -279,14 +305,21 @@ pub(super) fn material_element_with_role(
corners.rect.size, corners.rect.size,
); );
let background_opacity = material.opacity; let background_opacity = material.opacity;
let blur = material_blur_radius(material.style, material.opacity, state.theme_settings.backdrop_blur); let blur = material_blur_radius(
material.style,
material.opacity,
state.theme_settings.backdrop_blur,
);
let sample_geometry = expanded_blur_region(capture_geometry, blur.ceil() as i32, output_size); let sample_geometry = expanded_blur_region(capture_geometry, blur.ceil() as i32, output_size);
let sample_physical = sample_geometry.to_physical_precise_round(scale); let sample_physical = sample_geometry.to_physical_precise_round(scale);
let panel = (role == crate::effects::SemanticRole::Panel) let panel = (role == crate::effects::SemanticRole::Panel)
.then(|| crate::effects::surface_panel_presentation(surface)) .then(|| crate::effects::surface_panel_presentation(surface))
.flatten() .flatten()
.map(|presentation| panel_matrices(&presentation, shape_origin, scale, mode.size, transform)); .map(|presentation| {
let opaque = panel.is_none() && material.opacity == 1.0 && presentation_alpha == 1.0 && radius == 0.0; panel_matrices(&presentation, shape_origin, scale, mode.size, transform)
});
let opaque =
panel.is_none() && material.opacity == 1.0 && presentation_alpha == 1.0 && radius == 0.0;
let mut parameters = MaterialParameters { let mut parameters = MaterialParameters {
panel, panel,
corner_radii, corner_radii,
@@ -321,12 +354,16 @@ pub(super) fn material_element_with_role(
.get(&capture_key) .get(&capture_key)
.is_none_or(|c| c.geometry != sample_geometry || c.texture.size() != capture_size) .is_none_or(|c| c.geometry != sample_geometry || c.texture.size() != capture_size)
{ {
let texture = Offscreen::<GlesTexture>::create_buffer(renderer, Fourcc::Abgr8888, capture_size) let texture = Offscreen::<GlesTexture>::create_buffer(
.map_err(|error| { renderer,
tracing::warn!(%error, ?sample_physical, "backdrop capture allocation failed"); Fourcc::Abgr8888,
error capture_size,
}) )
.ok(); .map_err(|error| {
tracing::warn!(%error, ?sample_physical, "backdrop capture allocation failed");
error
})
.ok();
if let Some(texture) = texture { if let Some(texture) = texture {
buffers.captures.insert( buffers.captures.insert(
capture_key.clone(), capture_key.clone(),
@@ -364,7 +401,9 @@ pub(super) fn material_element_with_role(
MaterialElement::Fill(SharedPixelShaderElement::new( MaterialElement::Fill(SharedPixelShaderElement::new(
program.0.clone(), program.0.clone(),
geometry, geometry,
parameters.opaque.then(|| vec![Rectangle::from_size(geometry.size)]), parameters
.opaque
.then(|| vec![Rectangle::from_size(geometry.size)]),
1.0, 1.0,
material_uniforms(&parameters), material_uniforms(&parameters),
RenderElementKind::Unspecified, RenderElementKind::Unspecified,
@@ -404,13 +443,17 @@ pub(super) fn material_element_with_role(
|| cached.parameters.panel != parameters.panel || cached.parameters.panel != parameters.panel
{ {
cached.shadow.resize(parameters.shadow_bounds, None); cached.shadow.resize(parameters.shadow_bounds, None);
cached.shadow.update_uniforms(decoration_uniforms(&parameters, 2.0)); cached
.shadow
.update_uniforms(decoration_uniforms(&parameters, 2.0));
} }
match &mut cached.element { match &mut cached.element {
MaterialElement::Fill(element) => { MaterialElement::Fill(element) => {
element.resize( element.resize(
geometry, geometry,
parameters.opaque.then(|| vec![Rectangle::from_size(geometry.size)]), parameters
.opaque
.then(|| vec![Rectangle::from_size(geometry.size)]),
); );
element.update_uniforms(material_uniforms(&parameters)); element.update_uniforms(material_uniforms(&parameters));
} }
@@ -421,7 +464,9 @@ pub(super) fn material_element_with_role(
let background = match &cached.element { let background = match &cached.element {
// Keep the shader's opaque regions when its canvas already has the // Keep the shader's opaque regions when its canvas already has the
// shared edges. Animated modal geometry may need a physical override. // shared edges. Animated modal geometry may need a physical override.
MaterialElement::Fill(element) if element.geometry(scale.into()) == corners.rect => element.clone().into(), MaterialElement::Fill(element) if element.geometry(scale.into()) == corners.rect => {
element.clone().into()
}
MaterialElement::Fill(element) => PhysicalShaderElement { MaterialElement::Fill(element) => PhysicalShaderElement {
inner: element.clone(), inner: element.clone(),
geometry: corners.rect, geometry: corners.rect,
@@ -432,7 +477,11 @@ pub(super) fn material_element_with_role(
Some((background, cached.shadow.clone().into())) Some((background, cached.shadow.clone().into()))
} }
pub(super) fn material_blur_radius(style: crate::config::MaterialStyle, opacity: f32, radius: f64) -> f64 { pub(super) fn material_blur_radius(
style: crate::config::MaterialStyle,
opacity: f32,
radius: f64,
) -> f64 {
if style == crate::config::MaterialStyle::Translucent && opacity > 0.0 { if style == crate::config::MaterialStyle::Translucent && opacity > 0.0 {
radius radius
} else { } else {
@@ -440,7 +489,10 @@ pub(super) fn material_blur_radius(style: crate::config::MaterialStyle, opacity:
} }
} }
pub(super) fn blur_program(resources: &mut RenderResources, renderer: &mut GlesRenderer) -> Option<BlurProgram> { pub(super) fn blur_program(
resources: &mut RenderResources,
renderer: &mut GlesRenderer,
) -> Option<BlurProgram> {
let context = renderer.context_id().erased(); let context = renderer.context_id().erased();
if let Some(program) = resources if let Some(program) = resources
.contexts .contexts
@@ -449,7 +501,8 @@ pub(super) fn blur_program(resources: &mut RenderResources, renderer: &mut GlesR
{ {
return Some(program.clone()); return Some(program.clone());
} }
match renderer.compile_custom_texture_shader(corner_shader(BLUR_SHADER), &blur_uniform_names()) { match renderer.compile_custom_texture_shader(corner_shader(BLUR_SHADER), &blur_uniform_names())
{
Ok(program) => { Ok(program) => {
let program = BlurProgram(program); let program = BlurProgram(program);
resources.contexts.entry(context).or_default().blur = Some(program.clone()); resources.contexts.entry(context).or_default().blur = Some(program.clone());
@@ -481,8 +534,14 @@ pub(super) fn blur_uniforms(p: &MaterialParameters) -> Vec<Uniform<'static>> {
let mut uniforms = vec![ let mut uniforms = vec![
Uniform::new("visible_rect", p.visible_framebuffer), Uniform::new("visible_rect", p.visible_framebuffer),
Uniform::new("material_radii", p.corner_radii), Uniform::new("material_radii", p.corner_radii),
Uniform::new("texture_size", [p.sample_framebuffer[2], p.sample_framebuffer[3]]), Uniform::new(
Uniform::new("capture_origin", [p.sample_framebuffer[0], p.sample_framebuffer[1]]), "texture_size",
[p.sample_framebuffer[2], p.sample_framebuffer[3]],
),
Uniform::new(
"capture_origin",
[p.sample_framebuffer[0], p.sample_framebuffer[1]],
),
Uniform::new("blur_radius", p.blur), Uniform::new("blur_radius", p.blur),
Uniform::new("presentation_alpha", p.presentation_alpha), Uniform::new("presentation_alpha", p.presentation_alpha),
Uniform::new("background_opacity", p.background_opacity), Uniform::new("background_opacity", p.background_opacity),
@@ -524,10 +583,14 @@ pub(super) fn material_program_for_corners(
shape: CornerShape, shape: CornerShape,
) -> Option<MaterialProgram> { ) -> Option<MaterialProgram> {
let context = renderer.context_id().erased(); let context = renderer.context_id().erased();
if let Some(program) = resources.contexts.get(&context).and_then(|programs| match shape { if let Some(program) = resources
CornerShape::Circular => programs.material.as_ref(), .contexts
CornerShape::Continuous => programs.window_material.as_ref(), .get(&context)
}) { .and_then(|programs| match shape {
CornerShape::Circular => programs.material.as_ref(),
CornerShape::Continuous => programs.window_material.as_ref(),
})
{
return Some(program.clone()); return Some(program.clone());
} }
@@ -540,7 +603,8 @@ pub(super) fn material_program_for_corners(
UniformName::new("shadow_values", UniformType::_2f), UniformName::new("shadow_values", UniformType::_2f),
]; ];
uniforms.extend(panel_uniform_names()); uniforms.extend(panel_uniform_names());
match renderer.compile_custom_pixel_shader(corner_shader_for(MATERIAL_SHADER, shape), &uniforms) { match renderer.compile_custom_pixel_shader(corner_shader_for(MATERIAL_SHADER, shape), &uniforms)
{
Ok(program) => { Ok(program) => {
let program = MaterialProgram(program); let program = MaterialProgram(program);
let context = resources.contexts.entry(context).or_default(); let context = resources.contexts.entry(context).or_default();
@@ -561,7 +625,10 @@ pub(super) fn material_uniforms(parameters: &MaterialParameters) -> Vec<Uniform<
decoration_uniforms(parameters, 0.0) decoration_uniforms(parameters, 0.0)
} }
pub(super) fn decoration_uniforms(parameters: &MaterialParameters, paint_mode: f32) -> Vec<Uniform<'static>> { pub(super) fn decoration_uniforms(
parameters: &MaterialParameters,
paint_mode: f32,
) -> Vec<Uniform<'static>> {
let mut uniforms = vec![ let mut uniforms = vec![
Uniform::new("paint_mode", paint_mode), Uniform::new("paint_mode", paint_mode),
Uniform::new("shadow_rect", parameters.shadow_rect), Uniform::new("shadow_rect", parameters.shadow_rect),
@@ -587,9 +654,15 @@ fn panel_uniforms(parameters: &MaterialParameters) -> Vec<Uniform<'static>> {
let mut uniforms = vec![ let mut uniforms = vec![
Uniform::new( Uniform::new(
"panel_count", "panel_count",
parameters.panel.as_ref().map_or(0., |p| p.shapes.len() as f32), parameters
.panel
.as_ref()
.map_or(0., |p| p.shapes.len() as f32),
),
Uniform::new(
"panel_fusion",
parameters.panel.as_ref().map_or(0., |p| p.fusion),
), ),
Uniform::new("panel_fusion", parameters.panel.as_ref().map_or(0., |p| p.fusion)),
]; ];
if let Some(panel) = &parameters.panel if let Some(panel) = &parameters.panel
&& !panel.shapes.is_empty() && !panel.shapes.is_empty()
@@ -643,7 +716,11 @@ fn panel_matrices(
4 => 1, 4 => 1,
_ => 0, _ => 0,
}; };
match permutation.iter().position(|i| *i == old_corner as f32).unwrap() { match permutation
.iter()
.position(|i| *i == old_corner as f32)
.unwrap()
{
0 => 5, 0 => 5,
1 => 4, 1 => 4,
2 => 3, 2 => 3,
@@ -707,7 +784,10 @@ mod region_geometry_tests {
runtime.panel_style, runtime.panel_style,
); );
assert_eq!(material.opacity, if reduce { 1. } else { opacity }); assert_eq!(material.opacity, if reduce { 1. } else { opacity });
assert_eq!(material.opacity, candidate.theme.panel_opacity(Some(opacity))); assert_eq!(
material.opacity,
candidate.theme.panel_opacity(Some(opacity))
);
assert_eq!( assert_eq!(
material.style, material.style,
if reduce { if reduce {
@@ -738,9 +818,14 @@ mod region_geometry_tests {
let source = format!( let source = format!(
"theme {{ material {{ style translucent; opacity 0.6; blur-radius 20; }}; accessibility {{ reduce-transparency #{reduce}; }}; }}; panel main {{ style morph; }}" "theme {{ material {{ style translucent; opacity 0.6; blur-radius 20; }}; accessibility {{ reduce-transparency #{reduce}; }}; }}; panel main {{ style morph; }}"
); );
let (_, runtime, candidate) = crate::config::prepare(&source, std::path::Path::new("/tmp")).unwrap(); let (_, runtime, candidate) =
let material = crate::config::prepare(&source, std::path::Path::new("/tmp")).unwrap();
resolve_surface_material(SemanticRole::Panel, runtime.theme_settings, None, runtime.panel_style); let material = resolve_surface_material(
SemanticRole::Panel,
runtime.theme_settings,
None,
runtime.panel_style,
);
assert_eq!(material.shadow, [0.; 3]); assert_eq!(material.shadow, [0.; 3]);
assert_eq!(material.opacity, candidate.theme.panel_opacity(None)); assert_eq!(material.opacity, candidate.theme.panel_opacity(None));
assert_eq!( assert_eq!(
@@ -751,7 +836,11 @@ mod region_geometry_tests {
crate::config::MaterialStyle::Translucent crate::config::MaterialStyle::Translucent
} }
); );
let blur = material_blur_radius(material.style, material.opacity, runtime.theme_settings.backdrop_blur); let blur = material_blur_radius(
material.style,
material.opacity,
runtime.theme_settings.backdrop_blur,
);
assert_eq!(blur > 0., !reduce); assert_eq!(blur > 0., !reduce);
} }
} }
@@ -760,7 +849,10 @@ mod region_geometry_tests {
fn framebuffer_transforms_preserve_each_logical_corner() { fn framebuffer_transforms_preserve_each_logical_corner() {
let radii = [1., 2., 3., 4.]; let radii = [1., 2., 3., 4.];
assert_eq!(framebuffer_corner_radii(radii, Transform::Normal), radii); assert_eq!(framebuffer_corner_radii(radii, Transform::Normal), radii);
assert_eq!(framebuffer_corner_radii(radii, Transform::Flipped180), [4., 3., 2., 1.]); assert_eq!(
framebuffer_corner_radii(radii, Transform::Flipped180),
[4., 3., 2., 1.]
);
for (transform, expected) in [ for (transform, expected) in [
(Transform::Normal, [1., 2., 3., 4.]), (Transform::Normal, [1., 2., 3., 4.]),
(Transform::_90, [4., 1., 2., 3.]), (Transform::_90, [4., 1., 2., 3.]),
@@ -781,7 +873,9 @@ mod region_geometry_tests {
use ferese_panel::shape::PixelGrid; use ferese_panel::shape::PixelGrid;
for scale in [1., 1.25, 1.5, 2.] { for scale in [1., 1.25, 1.5, 2.] {
for origin in [-3, 0, 1, 3, 101] { for origin in [-3, 0, 1, 3, 101] {
for width in [149.99, 150., 150.01, 199.99, 200., 200.01, 259.99, 260., 260.01] { for width in [
149.99, 150., 150.01, 199.99, 200., 200.01, 259.99, 260., 260.01,
] {
let scene = derive( let scene = derive(
Parameters { Parameters {
style: ferese_panel::PanelStyle::Morph, style: ferese_panel::PanelStyle::Morph,
@@ -814,7 +908,8 @@ mod region_geometry_tests {
let [body, left, right] = shapes.as_slice() else { let [body, left, right] = shapes.as_slice() else {
panic!("expected body and shoulders") panic!("expected body and shoulders")
}; };
let client_left = (scene.paint[0].bounds.x * scale).round() + (f64::from(origin) * scale).round(); let client_left = (scene.paint[0].bounds.x * scale).round()
+ (f64::from(origin) * scale).round();
assert_eq!( assert_eq!(
body[0], client_left as f32, body[0], client_left as f32,
"native fill must align with the shell outline" "native fill must align with the shell outline"
@@ -864,7 +959,9 @@ mod region_geometry_tests {
attached: true, attached: true,
edge, edge,
fillet: 12., fillet: 12.,
radius: Some(AnchoredRadii(sample.geometry.radii.map(|r| r as f32))), radius: Some(AnchoredRadii(
sample.geometry.radii.map(|r| r as f32),
)),
..Default::default() ..Default::default()
}, },
bounds, bounds,
@@ -916,14 +1013,23 @@ mod region_geometry_tests {
#[test] #[test]
fn fractional_regions_snap_once_in_output_pixels() { fn fractional_regions_snap_once_in_output_pixels() {
let bounds = Rectangle::from_size((200, 100).into()); let bounds = Rectangle::from_size((200, 100).into());
let (_, corners) = material_region_geometry([0.4, 0.4, 100.4, 40.4, 14.0], bounds, (0, 0).into(), 1.5).unwrap(); let (_, corners) =
assert_eq!(corners.rect, Rectangle::new((1, 1).into(), (150, 60).into())); material_region_geometry([0.4, 0.4, 100.4, 40.4, 14.0], bounds, (0, 0).into(), 1.5)
.unwrap();
assert_eq!(
corners.rect,
Rectangle::new((1, 1).into(), (150, 60).into())
);
assert_eq!(corners.radius, 21.0); assert_eq!(corners.radius, 21.0);
// The old logical rounding would put this at physical (0, 0). // The old logical rounding would put this at physical (0, 0).
let (_, clipped) = let (_, clipped) =
material_region_geometry([-0.4, -0.4, 100.4, 40.4, 100.0], bounds, (0, 0).into(), 1.5).unwrap(); material_region_geometry([-0.4, -0.4, 100.4, 40.4, 100.0], bounds, (0, 0).into(), 1.5)
.unwrap();
assert_eq!(clipped.rect, Rectangle::from_size((150, 60).into())); assert_eq!(clipped.rect, Rectangle::from_size((150, 60).into()));
assert_eq!(clipped.radius, 30.0); assert_eq!(clipped.radius, 30.0);
assert!(material_region_geometry([300.0, 0.0, 10.0, 10.0, 2.0], bounds, (0, 0).into(), 1.5).is_none()); assert!(
material_region_geometry([300.0, 0.0, 10.0, 10.0, 2.0], bounds, (0, 0).into(), 1.5)
.is_none()
);
} }
} }
+155 -55
View File
@@ -19,14 +19,17 @@ use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration; use std::time::Duration;
pub(crate) use capture::{capture_resize_snapshot, capture_window_buffer, capture_window_frame, convert_window_pixels}; pub(crate) use capture::{
capture_resize_snapshot, capture_window_buffer, capture_window_frame, convert_window_pixels,
};
use decorations::*; use decorations::*;
use materials::*; use materials::*;
use overview::*; use overview::*;
pub(crate) use resources::RenderResources; pub(crate) use resources::RenderResources;
use scene::cursor_elements; use scene::cursor_elements;
pub(crate) use scene::{ pub(crate) use scene::{
animated_window_elements, capture_output_elements, frame_effect_metrics, layer_surfaces, sampled_output_elements, animated_window_elements, capture_output_elements, frame_effect_metrics, layer_surfaces,
sampled_output_elements,
}; };
pub(crate) use shader::SharedPixelShaderElement; pub(crate) use shader::SharedPixelShaderElement;
use smithay::backend::allocator::Fourcc; use smithay::backend::allocator::Fourcc;
@@ -36,40 +39,50 @@ use smithay::backend::renderer::element::surface::{
WaylandSurfaceRenderElement, WaylandSurfaceTexture, render_elements_from_surface_tree, WaylandSurfaceRenderElement, WaylandSurfaceTexture, render_elements_from_surface_tree,
}; };
use smithay::backend::renderer::element::utils::{ use smithay::backend::renderer::element::utils::{
ConstrainAlign, ConstrainScaleBehavior, CropRenderElement, Relocate, RelocateRenderElement, RescaleRenderElement, ConstrainAlign, ConstrainScaleBehavior, CropRenderElement, Relocate, RelocateRenderElement,
constrain_render_elements, RescaleRenderElement, constrain_render_elements,
}; };
use smithay::backend::renderer::element::{ use smithay::backend::renderer::element::{
Element, Id, Kind as RenderElementKind, RenderElement, UnderlyingStorage, render_elements, Element, Id, Kind as RenderElementKind, RenderElement, UnderlyingStorage, render_elements,
}; };
use smithay::backend::renderer::gles::{ use smithay::backend::renderer::gles::{
GlesError, GlesFrame, GlesPixelProgram, GlesRenderer, GlesTexProgram, GlesTexture, Uniform, UniformName, GlesError, GlesFrame, GlesPixelProgram, GlesRenderer, GlesTexProgram, GlesTexture, Uniform,
UniformType, UniformName, UniformType,
};
use smithay::backend::renderer::utils::{
CommitCounter, DamageSet, OpaqueRegions, draw_render_elements,
};
use smithay::backend::renderer::{
Bind, Color32F, ErasedContextId, ExportMem, Frame, Offscreen, Renderer, Texture,
}; };
use smithay::backend::renderer::utils::{CommitCounter, DamageSet, OpaqueRegions, draw_render_elements};
use smithay::backend::renderer::{Bind, Color32F, ErasedContextId, ExportMem, Frame, Offscreen, Renderer, Texture};
use smithay::desktop::space::{ConstrainBehavior, ConstrainReference, constrain_space_element}; use smithay::desktop::space::{ConstrainBehavior, ConstrainReference, constrain_space_element};
use smithay::desktop::{LayerSurface, PopupManager, layer_map_for_output}; use smithay::desktop::{LayerSurface, PopupManager, layer_map_for_output};
use smithay::input::pointer::{CursorImageStatus, CursorImageSurfaceData}; use smithay::input::pointer::{CursorImageStatus, CursorImageSurfaceData};
use smithay::output::Output; use smithay::output::Output;
use smithay::reexports::wayland_server::Resource; use smithay::reexports::wayland_server::Resource;
use smithay::utils::{Buffer, Logical, Physical, Point, Rectangle, Scale as RenderScale, Size, Transform}; use smithay::utils::{
Buffer, Logical, Physical, Point, Rectangle, Scale as RenderScale, Size, Transform,
};
use smithay::wayland::compositor::with_states; use smithay::wayland::compositor::with_states;
use smithay::wayland::shell::wlr_layer::Layer; use smithay::wayland::shell::wlr_layer::Layer;
use crate::Ferese; use crate::Ferese;
use crate::metrics::FrameEffectMetrics; use crate::metrics::FrameEffectMetrics;
use crate::presentation::{ use crate::presentation::{
CornerShape, NativeTextureElement, PhysicalShaderElement, RoundedRect, clamp_radius, corner_extent, physical_rect, CornerShape, NativeTextureElement, PhysicalShaderElement, RoundedRect, clamp_radius,
corner_extent, physical_rect,
}; };
type SurfaceRenderElement = type SurfaceRenderElement = CropRenderElement<
CropRenderElement<RelocateRenderElement<RescaleRenderElement<WaylandSurfaceRenderElement<GlesRenderer>>>>; RelocateRenderElement<RescaleRenderElement<WaylandSurfaceRenderElement<GlesRenderer>>>,
>;
type WindowRenderElement = CropRenderElement<RelocateRenderElement<RescaleRenderElement<WindowContentRenderElement>>>; type WindowRenderElement =
CropRenderElement<RelocateRenderElement<RescaleRenderElement<WindowContentRenderElement>>>;
type MemoryRenderElement = type MemoryRenderElement = CropRenderElement<
CropRenderElement<RelocateRenderElement<RescaleRenderElement<MemoryRenderBufferRenderElement<GlesRenderer>>>>; RelocateRenderElement<RescaleRenderElement<MemoryRenderBufferRenderElement<GlesRenderer>>>,
>;
render_elements! { render_elements! {
pub(crate) AnimatedWindowRenderElement<=GlesRenderer>; pub(crate) AnimatedWindowRenderElement<=GlesRenderer>;
@@ -127,7 +140,9 @@ fn corner_shader(source: &str) -> String {
fn corner_shader_for(source: &str, shape: CornerShape) -> String { fn corner_shader_for(source: &str, shape: CornerShape) -> String {
match shape { match shape {
CornerShape::Circular => source.replace("//_CORNERS_", include_str!("shaders/corners.glsl")), CornerShape::Circular => {
source.replace("//_CORNERS_", include_str!("shaders/corners.glsl"))
}
CornerShape::Continuous => source CornerShape::Continuous => source
.replace( .replace(
"//_CORNERS_", "//_CORNERS_",
@@ -138,7 +153,10 @@ fn corner_shader_for(source: &str, shape: CornerShape) -> String {
) )
.replace("precision mediump float;", "precision highp float;"), .replace("precision mediump float;", "precision highp float;"),
} }
.replace("//_MATERIAL_CORNERS_", include_str!("shaders/material_corners.glsl")) .replace(
"//_MATERIAL_CORNERS_",
include_str!("shaders/material_corners.glsl"),
)
.replace("//_PANEL_MASK_", include_str!("shaders/panel_mask.glsl")) .replace("//_PANEL_MASK_", include_str!("shaders/panel_mask.glsl"))
} }
@@ -216,7 +234,11 @@ impl Element for BlurRenderElement {
self.geometry.to_physical_precise_round(scale) self.geometry.to_physical_precise_round(scale)
} }
fn damage_since(&self, scale: RenderScale<f64>, commit: Option<CommitCounter>) -> DamageSet<i32, Physical> { fn damage_since(
&self,
scale: RenderScale<f64>,
commit: Option<CommitCounter>,
) -> DamageSet<i32, Physical> {
blur_damage(self.geometry(scale).size, self.current_commit(), commit) blur_damage(self.geometry(scale).size, self.current_commit(), commit)
} }
@@ -517,7 +539,11 @@ impl Element for RoundedSurfaceRenderElement {
self.inner.src() self.inner.src()
} }
fn damage_since(&self, scale: RenderScale<f64>, commit: Option<CommitCounter>) -> DamageSet<i32, Physical> { fn damage_since(
&self,
scale: RenderScale<f64>,
commit: Option<CommitCounter>,
) -> DamageSet<i32, Physical> {
clipped_surface_damage(self.inner.geometry(scale).size, self.clip_changed, || { clipped_surface_damage(self.inner.geometry(scale).size, self.clip_changed, || {
self.inner.damage_since(scale, commit) self.inner.damage_since(scale, commit)
}) })
@@ -634,8 +660,8 @@ mod tests {
use smithay::utils::{Buffer, Logical, Physical, Rectangle, Scale, Transform}; use smithay::utils::{Buffer, Logical, Physical, Rectangle, Scale, Transform};
use super::{ use super::{
border_gradient_line, color_with_alpha, framebuffer_clip_rect, resize_content_behavior, rounded_visual_rect, border_gradient_line, color_with_alpha, framebuffer_clip_rect, resize_content_behavior,
scaled_visual_rect, shadow_bounds, rounded_visual_rect, scaled_visual_rect, shadow_bounds,
}; };
#[test] #[test]
@@ -643,9 +669,14 @@ mod tests {
fn solid_corner_mask_preserves_premultiplied_color() { fn solid_corner_mask_preserves_premultiplied_color() {
use smithay::backend::allocator::Fourcc; use smithay::backend::allocator::Fourcc;
use smithay::backend::egl::{EGLContext, EGLDevice, EGLDisplay}; use smithay::backend::egl::{EGLContext, EGLDevice, EGLDisplay};
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture, Uniform, UniformName, UniformType}; use smithay::backend::renderer::gles::{
GlesRenderer, GlesTexture, Uniform, UniformName, UniformType,
};
use smithay::backend::renderer::{Bind, Color32F, ExportMem, Frame, Offscreen, Renderer}; use smithay::backend::renderer::{Bind, Color32F, ExportMem, Frame, Offscreen, Renderer};
let device = EGLDevice::enumerate().unwrap().last().expect("an EGL device"); let device = EGLDevice::enumerate()
.unwrap()
.last()
.expect("an EGL device");
let display = unsafe { EGLDisplay::new(device).unwrap() }; let display = unsafe { EGLDisplay::new(device).unwrap() };
let context = EGLContext::new(&display).unwrap(); let context = EGLContext::new(&display).unwrap();
let mut renderer = unsafe { GlesRenderer::new(context).unwrap() }; let mut renderer = unsafe { GlesRenderer::new(context).unwrap() };
@@ -662,7 +693,9 @@ mod tests {
let size = (32, 32).into(); let size = (32, 32).into();
let mut texture: GlesTexture = renderer.create_buffer(Fourcc::Abgr8888, size).unwrap(); let mut texture: GlesTexture = renderer.create_buffer(Fourcc::Abgr8888, size).unwrap();
let rect = Rectangle::<i32, Physical>::from_size((32, 32).into()); let rect = Rectangle::<i32, Physical>::from_size((32, 32).into());
for (radius, corner_alpha, inset) in [(12.0f32, 0u8, 0.0f32), (0.0, 64, 0.0), (0.0, 48, 0.25)] { for (radius, corner_alpha, inset) in
[(12.0f32, 0u8, 0.0f32), (0.0, 64, 0.0), (0.0, 48, 0.25)]
{
let mut target = renderer.bind(&mut texture).unwrap(); let mut target = renderer.bind(&mut texture).unwrap();
{ {
let mut frame = renderer let mut frame = renderer
@@ -678,7 +711,10 @@ mod tests {
Some(&[rect]), Some(&[rect]),
0.5, 0.5,
&[ &[
Uniform::new("clip_rect", [inset, inset, 32.0 - 2.0 * inset, 32.0 - 2.0 * inset]), Uniform::new(
"clip_rect",
[inset, inset, 32.0 - 2.0 * inset, 32.0 - 2.0 * inset],
),
Uniform::new("radius", radius), Uniform::new("radius", radius),
Uniform::new("color", [0.5f32, 0.0, 0.0, 0.5]), Uniform::new("color", [0.5f32, 0.0, 0.0, 0.5]),
], ],
@@ -702,7 +738,10 @@ mod tests {
fn shared_corner_shaders_compile() { fn shared_corner_shaders_compile() {
use smithay::backend::egl::{EGLContext, EGLDevice, EGLDisplay}; use smithay::backend::egl::{EGLContext, EGLDevice, EGLDisplay};
use smithay::backend::renderer::gles::GlesRenderer; use smithay::backend::renderer::gles::GlesRenderer;
let device = EGLDevice::enumerate().unwrap().last().expect("an EGL device"); let device = EGLDevice::enumerate()
.unwrap()
.last()
.expect("an EGL device");
let display = unsafe { EGLDisplay::new(device).unwrap() }; let display = unsafe { EGLDisplay::new(device).unwrap() };
let context = EGLContext::new(&display).unwrap(); let context = EGLContext::new(&display).unwrap();
let mut renderer = unsafe { GlesRenderer::new(context).unwrap() }; let mut renderer = unsafe { GlesRenderer::new(context).unwrap() };
@@ -729,22 +768,33 @@ mod tests {
use smithay::backend::allocator::Fourcc; use smithay::backend::allocator::Fourcc;
use smithay::backend::egl::{EGLContext, EGLDevice, EGLDisplay}; use smithay::backend::egl::{EGLContext, EGLDevice, EGLDisplay};
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture, Uniform}; use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture, Uniform};
use smithay::backend::renderer::{Bind, Color32F, ExportMem, Frame, ImportMem, Offscreen, Renderer}; use smithay::backend::renderer::{
Bind, Color32F, ExportMem, Frame, ImportMem, Offscreen, Renderer,
};
let device = EGLDevice::enumerate().unwrap().last().expect("an EGL device"); let device = EGLDevice::enumerate()
.unwrap()
.last()
.expect("an EGL device");
let display = unsafe { EGLDisplay::new(device).unwrap() }; let display = unsafe { EGLDisplay::new(device).unwrap() };
let context = EGLContext::new(&display).unwrap(); let context = EGLContext::new(&display).unwrap();
let mut renderer = unsafe { GlesRenderer::new(context).unwrap() }; let mut renderer = unsafe { GlesRenderer::new(context).unwrap() };
let program = renderer let program = renderer
.compile_custom_texture_shader(super::corner_shader(super::BLUR_SHADER), &super::blur_uniform_names()) .compile_custom_texture_shader(
super::corner_shader(super::BLUR_SHADER),
&super::blur_uniform_names(),
)
.unwrap(); .unwrap();
let size = (32, 32).into(); let size = (32, 32).into();
let mut pixels = vec![128u8; 32 * 32 * 4]; let mut pixels = vec![128u8; 32 * 32 * 4];
for pixel in pixels.as_chunks_mut::<4>().0 { for pixel in pixels.as_chunks_mut::<4>().0 {
pixel[3] = 255; pixel[3] = 255;
} }
let blurred = renderer.import_memory(&pixels, Fourcc::Abgr8888, size, false).unwrap(); let blurred = renderer
let mut target_texture: GlesTexture = renderer.create_buffer(Fourcc::Abgr8888, size).unwrap(); .import_memory(&pixels, Fourcc::Abgr8888, size, false)
.unwrap();
let mut target_texture: GlesTexture =
renderer.create_buffer(Fourcc::Abgr8888, size).unwrap();
let damage = Rectangle::<i32, Physical>::from_size((32, 32).into()); let damage = Rectangle::<i32, Physical>::from_size((32, 32).into());
for (opacity, strength, tint) in [ for (opacity, strength, tint) in [
@@ -768,7 +818,9 @@ mod tests {
let mut frame = renderer let mut frame = renderer
.render(&mut target, (32, 32).into(), Transform::Normal) .render(&mut target, (32, 32).into(), Transform::Normal)
.unwrap(); .unwrap();
frame.clear(Color32F::new(0.0, 0.0, 0.0, 1.0), &[damage]).unwrap(); frame
.clear(Color32F::new(0.0, 0.0, 0.0, 1.0), &[damage])
.unwrap();
frame frame
.draw_solid( .draw_solid(
Rectangle::new((0, 0).into(), (16, 32).into()), Rectangle::new((0, 0).into(), (16, 32).into()),
@@ -801,7 +853,8 @@ mod tests {
left.abs_diff(right) <= 2, left.abs_diff(right) <= 2,
"opacity {opacity} leaked the original backdrop: {left} versus {right}" "opacity {opacity} leaked the original backdrop: {left} versus {right}"
); );
let expected = (128.0 * (1.0 - strength * opacity) + tint * 255.0 * strength * opacity).round() as u8; let expected = (128.0 * (1.0 - strength * opacity) + tint * 255.0 * strength * opacity)
.round() as u8;
assert!( assert!(
left.abs_diff(expected) <= 2, left.abs_diff(expected) <= 2,
"opacity {opacity} failed to control tint: {left} versus {expected}" "opacity {opacity} failed to control tint: {left} versus {expected}"
@@ -823,7 +876,8 @@ mod tests {
Transform::Flipped270, Transform::Flipped270,
] { ] {
let area = transform.transform_size(mode).to_f64(); let area = transform.transform_size(mode).to_f64();
let rect: Rectangle<i32, Physical> = Rectangle::new((110, 60).into(), (800, 600).into()); let rect: Rectangle<i32, Physical> =
Rectangle::new((110, 60).into(), (800, 600).into());
for (angle, first, last) in [ for (angle, first, last) in [
(0.0, (110.0, 360.0), (910.0, 360.0)), (0.0, (110.0, 360.0), (910.0, 360.0)),
(90.0, (510.0, 60.0), (510.0, 660.0)), (90.0, (510.0, 60.0), (510.0, 660.0)),
@@ -836,8 +890,14 @@ mod tests {
(point.x - f64::from(line[0])) * f64::from(line[2]) (point.x - f64::from(line[0])) * f64::from(line[2])
+ (point.y - f64::from(line[1])) * f64::from(line[3]) + (point.y - f64::from(line[1])) * f64::from(line[3])
}; };
assert!(progress(first.into()).abs() < 0.00001, "{transform:?}, {angle}"); assert!(
assert!((progress(last.into()) - 1.0).abs() < 0.00001, "{transform:?}, {angle}"); progress(first.into()).abs() < 0.00001,
"{transform:?}, {angle}"
);
assert!(
(progress(last.into()) - 1.0).abs() < 0.00001,
"{transform:?}, {angle}"
);
} }
} }
} }
@@ -888,7 +948,12 @@ mod tests {
} }
fn src(&self) -> Rectangle<f64, Buffer> { fn src(&self) -> Rectangle<f64, Buffer> {
Rectangle::from_size(self.geometry.size.to_f64().to_buffer(1.0, Transform::Normal)) Rectangle::from_size(
self.geometry
.size
.to_f64()
.to_buffer(1.0, Transform::Normal),
)
} }
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> { fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
@@ -904,9 +969,9 @@ mod tests {
if commit == Some(self.current_commit()) { if commit == Some(self.current_commit()) {
smithay::backend::renderer::utils::DamageSet::default() smithay::backend::renderer::utils::DamageSet::default()
} else { } else {
smithay::backend::renderer::utils::DamageSet::from_slice(&[Rectangle::from_size( smithay::backend::renderer::utils::DamageSet::from_slice(&[
self.geometry(scale).size, Rectangle::from_size(self.geometry(scale).size),
)]) ])
} }
}) })
} }
@@ -933,7 +998,12 @@ mod tests {
element.clip_changed = resources.thumbnail_corner_changed(window, output, shape); element.clip_changed = resources.thumbnail_corner_changed(window, output, shape);
assert_eq!(element.commit, client_commit); assert_eq!(element.commit, client_commit);
assert!(element.clip_changed); assert!(element.clip_changed);
let damage = tracker.damage_output(1, &[&element]).unwrap().0.cloned().unwrap(); let damage = tracker
.damage_output(1, &[&element])
.unwrap()
.0
.cloned()
.unwrap();
assert!(damage.iter().any(|rect| rect.contains((100, 100)))); assert!(damage.iter().any(|rect| rect.contains((100, 100))));
assert!(damage.iter().any(|rect| rect.contains((299, 259)))); assert!(damage.iter().any(|rect| rect.contains((299, 259))));
element.clip_changed = resources.thumbnail_corner_changed(window, output, shape); element.clip_changed = resources.thumbnail_corner_changed(window, output, shape);
@@ -949,7 +1019,9 @@ mod tests {
#[test] #[test]
fn resize_keeps_source_pixels_native_when_growing_and_shrinking() { fn resize_keeps_source_pixels_native_when_growing_and_shrinking() {
use smithay::backend::renderer::element::utils::{ConstrainAlign, constrain_render_elements}; use smithay::backend::renderer::element::utils::{
ConstrainAlign, constrain_render_elements,
};
for scale in [1.0, 1.5, 1.8, 2.0] { for scale in [1.0, 1.5, 1.8, 2.0] {
for width in [300, 600, 1200] { for width in [300, 600, 1200] {
let source = Rectangle::<i32, Logical>::from_size((600, 800).into()); let source = Rectangle::<i32, Logical>::from_size((600, 800).into());
@@ -1020,14 +1092,18 @@ mod tests {
Transform::Flipped180, Transform::Flipped180,
Transform::Flipped270, Transform::Flipped270,
] { ] {
let output_size = let output_size = smithay::utils::Size::<i32, Physical>::from((
smithay::utils::Size::<i32, Physical>::from(((1280.0 * scale) as i32, (800.0 * scale) as i32)); (1280.0 * scale) as i32,
(800.0 * scale) as i32,
));
let element_size = transform.transform_size(output_size); let element_size = transform.transform_size(output_size);
let logical_height = f64::from(element_size.h) / scale; let logical_height = f64::from(element_size.h) / scale;
// Bar and exclusion end at 40; the 10px outer gap ends at 50. // Bar and exclusion end at 40; the 10px outer gap ends at 50.
let geometry = let geometry = Rectangle::<f64, Logical>::new(
Rectangle::<f64, Logical>::new((10.0, 50.0).into(), (400.0, logical_height - 60.0).into()) (10.0, 50.0).into(),
.to_physical_precise_round(scale); (400.0, logical_height - 60.0).into(),
)
.to_physical_precise_round(scale);
let clip = framebuffer_clip_rect(geometry, output_size, transform); let clip = framebuffer_clip_rect(geometry, output_size, transform);
// Independently reproduce GlesFrame's orthographic projection, // Independently reproduce GlesFrame's orthographic projection,
// transform matrix, GL flip, and viewport mapping for each corner. // transform matrix, GL flip, and viewport mapping for each corner.
@@ -1048,7 +1124,10 @@ mod tests {
let max_x = xs.iter().copied().fold(f32::NEG_INFINITY, f32::max); let max_x = xs.iter().copied().fold(f32::NEG_INFINITY, f32::max);
let min_y = ys.iter().copied().fold(f32::INFINITY, f32::min); let min_y = ys.iter().copied().fold(f32::INFINITY, f32::min);
let max_y = ys.iter().copied().fold(f32::NEG_INFINITY, f32::max); let max_y = ys.iter().copied().fold(f32::NEG_INFINITY, f32::max);
for (actual, expected) in clip.into_iter().zip([min_x, min_y, max_x - min_x, max_y - min_y]) { for (actual, expected) in
clip.into_iter()
.zip([min_x, min_y, max_x - min_x, max_y - min_y])
{
assert!( assert!(
(actual - expected).abs() < 0.001, (actual - expected).abs() < 0.001,
"{transform:?}, scale={scale}: {actual} != {expected}" "{transform:?}, scale={scale}: {actual} != {expected}"
@@ -1077,11 +1156,17 @@ mod tests {
let old_window = Rectangle::<i32, Logical>::new((100, 80).into(), (400, 300).into()); let old_window = Rectangle::<i32, Logical>::new((100, 80).into(), (400, 300).into());
let new_window = Rectangle::<i32, Logical>::new((300, 180).into(), (400, 300).into()); let new_window = Rectangle::<i32, Logical>::new((300, 180).into(), (400, 300).into());
let mut shadow = DamageElement::new(shadow_bounds(old_window, 4.0, 18.0)); let mut shadow = DamageElement::new(shadow_bounds(old_window, 4.0, 18.0));
let mut tracker = OutputDamageTracker::new((1_000, 800), Scale::from(1.0), Transform::Normal); let mut tracker =
OutputDamageTracker::new((1_000, 800), Scale::from(1.0), Transform::Normal);
tracker.damage_output(0, &[&shadow]).unwrap(); tracker.damage_output(0, &[&shadow]).unwrap();
shadow.geometry = shadow_bounds(new_window, 4.0, 18.0); shadow.geometry = shadow_bounds(new_window, 4.0, 18.0);
let damage = tracker.damage_output(1, &[&shadow]).unwrap().0.cloned().unwrap(); let damage = tracker
.damage_output(1, &[&shadow])
.unwrap()
.0
.cloned()
.unwrap();
assert!(damage.iter().any(|rect| rect.contains((64, 48)))); assert!(damage.iter().any(|rect| rect.contains((64, 48))));
assert!(damage.iter().any(|rect| rect.contains((735, 519)))); assert!(damage.iter().any(|rect| rect.contains((735, 519))));
@@ -1098,13 +1183,22 @@ mod tests {
#[test] #[test]
fn translucent_materials_blur_at_full_opacity() { fn translucent_materials_blur_at_full_opacity() {
use crate::config::MaterialStyle; use crate::config::MaterialStyle;
assert_eq!(super::material_blur_radius(MaterialStyle::Translucent, 0.0, 18.0), 0.0); assert_eq!(
assert_eq!(super::material_blur_radius(MaterialStyle::Translucent, 1.0, 18.0), 18.0); super::material_blur_radius(MaterialStyle::Translucent, 0.0, 18.0),
0.0
);
assert_eq!(
super::material_blur_radius(MaterialStyle::Translucent, 1.0, 18.0),
18.0
);
assert_eq!( assert_eq!(
super::material_blur_radius(MaterialStyle::Translucent, 0.79, 18.0), super::material_blur_radius(MaterialStyle::Translucent, 0.79, 18.0),
18.0 18.0
); );
assert_eq!(super::material_blur_radius(MaterialStyle::Solid, 0.79, 18.0), 0.0); assert_eq!(
super::material_blur_radius(MaterialStyle::Solid, 0.79, 18.0),
0.0
);
} }
#[test] #[test]
@@ -1120,7 +1214,10 @@ mod tests {
#[test] #[test]
fn close_opacity_only_changes_the_alpha_channel() { fn close_opacity_only_changes_the_alpha_channel() {
assert_eq!(color_with_alpha([0.2, 0.4, 0.6, 0.8], 0.5), [0.2, 0.4, 0.6, 0.4]); assert_eq!(
color_with_alpha([0.2, 0.4, 0.6, 0.8], 0.5),
[0.2, 0.4, 0.6, 0.4]
);
} }
#[test] #[test]
@@ -1134,7 +1231,10 @@ mod tests {
for tenth in 0..=10 { for tenth in 0..=10 {
let left = 100.0 + f64::from(tenth) / 10.0; let left = 100.0 + f64::from(tenth) / 10.0;
let frame = rounded_visual_rect(ferese_layout::Rect::new(left, 50.0, 500.0 - left, 300.0), (0, 0).into()); let frame = rounded_visual_rect(
ferese_layout::Rect::new(left, 50.0, 500.0 - left, 300.0),
(0, 0).into(),
);
assert_eq!(frame.loc.x + frame.size.w, 500); assert_eq!(frame.loc.x + frame.size.w, 500);
} }
+57 -21
View File
@@ -19,7 +19,11 @@ pub(super) fn overview_chrome_element(
let mode = output.current_mode()?; let mode = output.current_mode()?;
let parameters = BorderParameters { let parameters = BorderParameters {
geometry, geometry,
clip_rect: framebuffer_clip_rect(corners.rect, mode.size, output.current_transform().invert()), clip_rect: framebuffer_clip_rect(
corners.rect,
mode.size,
output.current_transform().invert(),
),
radius: corners.radius, radius: corners.radius,
width: (width * scale) as f32, width: (width * scale) as f32,
color, color,
@@ -30,13 +34,19 @@ pub(super) fn overview_chrome_element(
focus_gradient_line: [0.0; 4], focus_gradient_line: [0.0; 4],
focus_mix: 0.0, focus_mix: 0.0,
}; };
let program = if matches!(key.1, OverviewChromePart::Outline | OverviewChromePart::TabListOutline) { let program = if matches!(
key.1,
OverviewChromePart::Outline | OverviewChromePart::TabListOutline
) {
rounded_clip_program(&mut state.render, renderer)?.border rounded_clip_program(&mut state.render, renderer)?.border
} else { } else {
material_program(&mut state.render, renderer)?.0 material_program(&mut state.render, renderer)?.0
}; };
let uniforms = |p: &BorderParameters| { let uniforms = |p: &BorderParameters| {
if matches!(key.1, OverviewChromePart::Outline | OverviewChromePart::TabListOutline) { if matches!(
key.1,
OverviewChromePart::Outline | OverviewChromePart::TabListOutline
) {
border_uniforms(p) border_uniforms(p)
} else { } else {
tint_uniforms(p) tint_uniforms(p)
@@ -47,7 +57,9 @@ pub(super) fn overview_chrome_element(
.render .render
.outputs .outputs
.entry(output_id) .entry(output_id)
.or_insert_with(|| OverviewScrim { chrome: HashMap::new() }) .or_insert_with(|| OverviewScrim {
chrome: HashMap::new(),
})
.chrome; .chrome;
let cached = chrome let cached = chrome
.entry(key) .entry(key)
@@ -129,7 +141,9 @@ pub(super) fn overview_strip_elements(
| OverviewChromePart::TabListOutline | OverviewChromePart::TabListOutline
| OverviewChromePart::TabList | OverviewChromePart::TabList
| OverviewChromePart::TabRow | OverviewChromePart::TabRow
| OverviewChromePart::TabFlash => state.windows.window(ferese_layout::WindowId(*id)).is_some(), | OverviewChromePart::TabFlash => {
state.windows.window(ferese_layout::WindowId(*id)).is_some()
}
OverviewChromePart::Card | OverviewChromePart::Outline => card_ids.contains(id), OverviewChromePart::Card | OverviewChromePart::Outline => card_ids.contains(id),
OverviewChromePart::Strip => true, OverviewChromePart::Strip => true,
}); });
@@ -149,7 +163,11 @@ pub(super) fn overview_strip_elements(
{ {
continue; continue;
} }
let Some(rect) = frame.windows.get(&id).map(|sample| sample.presentation.bounds.current) else { let Some(rect) = frame
.windows
.get(&id)
.map(|sample| sample.presentation.bounds.current)
else {
continue; continue;
}; };
let Some((buffer, size)) = state.overview_window_label(id, scale, rect.width) else { let Some((buffer, size)) = state.overview_window_label(id, scale, rect.width) else {
@@ -163,7 +181,9 @@ pub(super) fn overview_strip_elements(
26.0, 26.0,
); );
let location = Point::<i32, Physical>::from(( let location = Point::<i32, Physical>::from((
((caption.x - f64::from(output_geometry.loc.x) + caption.width * 0.5) * scale).round() as i32 - size.w / 2, ((caption.x - f64::from(output_geometry.loc.x) + caption.width * 0.5) * scale).round()
as i32
- size.w / 2,
((caption.y - f64::from(output_geometry.loc.y) + 4.0) * scale).round() as i32, ((caption.y - f64::from(output_geometry.loc.y) + 4.0) * scale).round() as i32,
)); ));
if let Ok(element) = MemoryRenderBufferRenderElement::from_buffer( if let Ok(element) = MemoryRenderBufferRenderElement::from_buffer(
@@ -171,7 +191,9 @@ pub(super) fn overview_strip_elements(
location.to_f64(), location.to_f64(),
&buffer, &buffer,
Some(alpha), Some(alpha),
Some(Rectangle::from_size((f64::from(size.w), f64::from(size.h)).into())), Some(Rectangle::from_size(
(f64::from(size.w), f64::from(size.h)).into(),
)),
Some( Some(
( (
(f64::from(size.w) / scale).round() as i32, (f64::from(size.w) / scale).round() as i32,
@@ -183,9 +205,11 @@ pub(super) fn overview_strip_elements(
) { ) {
let element = RescaleRenderElement::from_element(element, location, 1.0); let element = RescaleRenderElement::from_element(element, location, 1.0);
let element = RelocateRenderElement::from_element(element, (0, 0), Relocate::Relative); let element = RelocateRenderElement::from_element(element, (0, 0), Relocate::Relative);
if let Some(element) = if let Some(element) = CropRenderElement::from_element(
CropRenderElement::from_element(element, scale, physical_rect(caption, output_geometry.loc, scale)) element,
{ scale,
physical_rect(caption, output_geometry.loc, scale),
) {
elements.push(element.into()); elements.push(element.into());
} }
} }
@@ -235,13 +259,20 @@ pub(super) fn overview_strip_elements(
let shape = window_corner_shape(window); let shape = window_corner_shape(window);
if let Some(programs) = corner_program(&mut state.render, renderer, shape) { if let Some(programs) = corner_program(&mut state.render, renderer, shape) {
let clip_changed = state let clip_changed = state.output_id(output).is_some_and(|output| {
.output_id(output) state.render.thumbnail_corner_changed(*id, output, shape)
.is_some_and(|output| state.render.thumbnail_corner_changed(*id, output, shape)); });
let Some(sample) = frame.windows.get(id) else { continue }; let Some(sample) = frame.windows.get(id) else {
continue;
};
let presentation = sample.presentation.thumbnail(*rect); let presentation = sample.presentation.thumbnail(*rect);
let corners = RoundedRect::new(presentation.bounds.current, output_geometry.loc, scale, 4.0) let corners = RoundedRect::new(
.with_shape(shape); presentation.bounds.current,
output_geometry.loc,
scale,
4.0,
)
.with_shape(shape);
if let Some(snapshot) = state.render.snapshot(id) if let Some(snapshot) = state.render.snapshot(id)
&& snapshot.context == renderer.context_id().erased() && snapshot.context == renderer.context_id().erased()
&& (snapshot.scale - scale).abs() < 0.001 && (snapshot.scale - scale).abs() < 0.001
@@ -277,16 +308,20 @@ pub(super) fn overview_strip_elements(
} }
if let Some((buffer, size)) = state.overview_workspace_label(card.index, scale) { if let Some((buffer, size)) = state.overview_workspace_label(card.index, scale) {
let location = Point::<i32, Physical>::from(( let location = Point::<i32, Physical>::from((
((card.rect.x - f64::from(output_geometry.loc.x) + card.rect.width * 0.5) * scale).round() as i32 ((card.rect.x - f64::from(output_geometry.loc.x) + card.rect.width * 0.5) * scale)
.round() as i32
- size.w / 2, - size.w / 2,
((card.rect.y - f64::from(output_geometry.loc.y) + card.rect.height - 20.0) * scale).round() as i32, ((card.rect.y - f64::from(output_geometry.loc.y) + card.rect.height - 20.0) * scale)
.round() as i32,
)); ));
if let Ok(element) = MemoryRenderBufferRenderElement::from_buffer( if let Ok(element) = MemoryRenderBufferRenderElement::from_buffer(
renderer, renderer,
location.to_f64(), location.to_f64(),
&buffer, &buffer,
Some(alpha), Some(alpha),
Some(Rectangle::from_size((f64::from(size.w), f64::from(size.h)).into())), Some(Rectangle::from_size(
(f64::from(size.w), f64::from(size.h)).into(),
)),
Some( Some(
( (
(f64::from(size.w) / scale).round() as i32, (f64::from(size.w) / scale).round() as i32,
@@ -297,7 +332,8 @@ pub(super) fn overview_strip_elements(
RenderElementKind::Unspecified, RenderElementKind::Unspecified,
) { ) {
let element = RescaleRenderElement::from_element(element, location, 1.0); let element = RescaleRenderElement::from_element(element, location, 1.0);
let element = RelocateRenderElement::from_element(element, (0, 0), Relocate::Relative); let element =
RelocateRenderElement::from_element(element, (0, 0), Relocate::Relative);
if let Some(element) = CropRenderElement::from_element( if let Some(element) = CropRenderElement::from_element(
element, element,
scale, scale,
+31 -8
View File
@@ -60,7 +60,12 @@ impl RenderResources {
changed changed
} }
pub(super) fn thumbnail_corner_changed(&mut self, id: WindowId, output: OutputId, shape: CornerShape) -> bool { pub(super) fn thumbnail_corner_changed(
&mut self,
id: WindowId,
output: OutputId,
shape: CornerShape,
) -> bool {
self.windows self.windows
.entry(id) .entry(id)
.or_default() .or_default()
@@ -117,7 +122,9 @@ impl RenderResources {
} }
pub fn snapshots(&self) -> impl Iterator<Item = &ResizeSnapshot> { pub fn snapshots(&self) -> impl Iterator<Item = &ResizeSnapshot> {
self.windows.values().filter_map(|window| window.snapshot.as_ref()) self.windows
.values()
.filter_map(|window| window.snapshot.as_ref())
} }
pub fn set_snapshot(&mut self, id: WindowId, snapshot: ResizeSnapshot) { pub fn set_snapshot(&mut self, id: WindowId, snapshot: ResizeSnapshot) {
@@ -130,7 +137,10 @@ impl RenderResources {
} }
} }
pub fn retain_snapshots(&mut self, mut keep: impl FnMut(&WindowId, &mut ResizeSnapshot) -> bool) { pub fn retain_snapshots(
&mut self,
mut keep: impl FnMut(&WindowId, &mut ResizeSnapshot) -> bool,
) {
for (id, window) in &mut self.windows { for (id, window) in &mut self.windows {
if let Some(snapshot) = &mut window.snapshot if let Some(snapshot) = &mut window.snapshot
&& !keep(id, snapshot) && !keep(id, snapshot)
@@ -159,7 +169,8 @@ impl RenderResources {
.filter(|window| &window.snapshot.context == context) .filter(|window| &window.snapshot.context == context)
.map(|window| window.presentation.id) .map(|window| window.presentation.id)
.collect(); .collect();
self.closing.retain(|window| &window.snapshot.context != context); self.closing
.retain(|window| &window.snapshot.context != context);
for id in removed { for id in removed {
self.remove_window(id); self.remove_window(id);
} }
@@ -201,8 +212,12 @@ mod tests {
#[test] #[test]
fn window_removal_releases_all_its_caches_without_removing_other_windows() { fn window_removal_releases_all_its_caches_without_removing_other_windows() {
let mut resources = RenderResources::default(); let mut resources = RenderResources::default();
resources.windows.insert(WindowId(1), WindowBuffers::default()); resources
resources.windows.insert(WindowId(2), WindowBuffers::default()); .windows
.insert(WindowId(1), WindowBuffers::default());
resources
.windows
.insert(WindowId(2), WindowBuffers::default());
resources.remove_window(WindowId(1)); resources.remove_window(WindowId(1));
resources.clear_snapshot(&WindowId(1)); resources.clear_snapshot(&WindowId(1));
@@ -217,7 +232,10 @@ mod tests {
fn context_removal_evicts_programs_and_snapshots_without_touching_another_gpu() { fn context_removal_evicts_programs_and_snapshots_without_touching_another_gpu() {
use smithay::backend::egl::{EGLContext, EGLDevice, EGLDisplay}; use smithay::backend::egl::{EGLContext, EGLDevice, EGLDisplay};
let device = EGLDevice::enumerate().unwrap().last().expect("an EGL device"); let device = EGLDevice::enumerate()
.unwrap()
.last()
.expect("an EGL device");
let display = unsafe { EGLDisplay::new(device).unwrap() }; let display = unsafe { EGLDisplay::new(device).unwrap() };
let mut first = unsafe { GlesRenderer::new(EGLContext::new(&display).unwrap()).unwrap() }; let mut first = unsafe { GlesRenderer::new(EGLContext::new(&display).unwrap()).unwrap() };
let mut second = unsafe { GlesRenderer::new(EGLContext::new(&display).unwrap()).unwrap() }; let mut second = unsafe { GlesRenderer::new(EGLContext::new(&display).unwrap()).unwrap() };
@@ -229,7 +247,12 @@ mod tests {
rounded_clip_program(&mut resources, renderer).unwrap(); rounded_clip_program(&mut resources, renderer).unwrap();
material_program(&mut resources, renderer).unwrap(); material_program(&mut resources, renderer).unwrap();
blur_program(&mut resources, renderer).unwrap(); blur_program(&mut resources, renderer).unwrap();
let texture = Offscreen::<GlesTexture>::create_buffer(renderer, Fourcc::Abgr8888, (16, 16).into()).unwrap(); let texture = Offscreen::<GlesTexture>::create_buffer(
renderer,
Fourcc::Abgr8888,
(16, 16).into(),
)
.unwrap();
resources.set_snapshot( resources.set_snapshot(
id, id,
ResizeSnapshot { ResizeSnapshot {
+91 -36
View File
@@ -10,7 +10,10 @@ pub(crate) fn animated_window_elements(
output_elements(state, renderer, output, true) output_elements(state, renderer, output, true)
} }
pub(crate) fn frame_effect_metrics(_elements: &[AnimatedWindowRenderElement], _scale: f64) -> FrameEffectMetrics { pub(crate) fn frame_effect_metrics(
_elements: &[AnimatedWindowRenderElement],
_scale: f64,
) -> FrameEffectMetrics {
FrameEffectMetrics FrameEffectMetrics
} }
@@ -69,9 +72,16 @@ fn scene_elements(
} else { } else {
Vec::new() Vec::new()
}; };
let background = state.session_lock.backgrounds.entry(output.clone()).or_insert_with(|| { let background = state
smithay::backend::renderer::element::solid::SolidColorBuffer::new(geometry.size, [0.0, 0.0, 0.0, 1.0]) .session_lock
}); .backgrounds
.entry(output.clone())
.or_insert_with(|| {
smithay::backend::renderer::element::solid::SolidColorBuffer::new(
geometry.size,
[0.0, 0.0, 0.0, 1.0],
)
});
background.resize(geometry.size); background.resize(geometry.size);
let opacity = state.session_lock.idle_opacity; let opacity = state.session_lock.idle_opacity;
if opacity > 0.0 { if opacity > 0.0 {
@@ -87,8 +97,14 @@ fn scene_elements(
}); });
overlay.resize(geometry.size); overlay.resize(geometry.size);
elements.push( elements.push(
SolidColorRenderElement::from_buffer(overlay, (0, 0), scale, opacity, RenderElementKind::Unspecified) SolidColorRenderElement::from_buffer(
.into(), overlay,
(0, 0),
scale,
opacity,
RenderElementKind::Unspecified,
)
.into(),
); );
} }
if let Some(surface) = state if let Some(surface) = state
@@ -98,7 +114,10 @@ fn scene_elements(
.filter(|surface| surface.alive()) .filter(|surface| surface.alive())
{ {
elements.extend( elements.extend(
render_elements_from_surface_tree::<GlesRenderer, WaylandSurfaceRenderElement<GlesRenderer>>( render_elements_from_surface_tree::<
GlesRenderer,
WaylandSurfaceRenderElement<GlesRenderer>,
>(
renderer, renderer,
surface.wl_surface(), surface.wl_surface(),
(0, 0), (0, 0),
@@ -111,7 +130,14 @@ fn scene_elements(
); );
} }
elements.push( elements.push(
SolidColorRenderElement::from_buffer(background, (0, 0), scale, 1.0, RenderElementKind::Unspecified).into(), SolidColorRenderElement::from_buffer(
background,
(0, 0),
scale,
1.0,
RenderElementKind::Unspecified,
)
.into(),
); );
return elements; return elements;
} }
@@ -126,11 +152,12 @@ fn scene_elements(
} else { } else {
Vec::new() Vec::new()
}; };
let upper_layers: &[Layer] = if !state.layer_visible_for_frame(output, Layer::Top, frame.overview.is_presenting()) { let upper_layers: &[Layer] =
&[Layer::Overlay] if !state.layer_visible_for_frame(output, Layer::Top, frame.overview.is_presenting()) {
} else { &[Layer::Overlay]
&[Layer::Overlay, Layer::Top] } else {
}; &[Layer::Overlay, Layer::Top]
};
elements.extend(layer_elements(state, renderer, output, upper_layers)); elements.extend(layer_elements(state, renderer, output, upper_layers));
if frame.overview.is_presenting() { if frame.overview.is_presenting() {
elements.extend(overview_strip_elements( elements.extend(overview_strip_elements(
@@ -196,7 +223,9 @@ fn scene_elements(
if let Some(group) = closing.remove(&Some(id)) { if let Some(group) = closing.remove(&Some(id)) {
elements.extend(group); elements.extend(group);
} }
elements.extend(super::tabs::tab_elements(state, renderer, output, frame, id, capture)); elements.extend(super::tabs::tab_elements(
state, renderer, output, frame, id, capture,
));
let constrain = rounded_visual_rect(visual, output_geometry.loc); let constrain = rounded_visual_rect(visual, output_geometry.loc);
let material_surface = window let material_surface = window
.toplevel() .toplevel()
@@ -211,7 +240,8 @@ fn scene_elements(
state.theme_settings.window_radius state.theme_settings.window_radius
} * decoration_progress; } * decoration_progress;
let shape = window_corner_shape(&window); let shape = window_corner_shape(&window);
let corners = RoundedRect::new(visual, output_geometry.loc, scale, window_radius).with_shape(shape); let corners =
RoundedRect::new(visual, output_geometry.loc, scale, window_radius).with_shape(shape);
let corner_shape_changed = state.render.prepare_window_corners(id, shape); let corner_shape_changed = state.render.prepare_window_corners(id, shape);
let rounded_clip_program = corner_program(&mut state.render, renderer, shape); let rounded_clip_program = corner_program(&mut state.render, renderer, shape);
// Only overview/close intentionally scale the complete application. // Only overview/close intentionally scale the complete application.
@@ -241,7 +271,8 @@ fn scene_elements(
let shadow_color = state.theme_settings.shadow_color.0; let shadow_color = state.theme_settings.shadow_color.0;
let focus = sample.presentation.focus(); let focus = sample.presentation.focus();
let border_width = state.theme_settings.border_width let border_width = state.theme_settings.border_width
+ (state.theme_settings.focus_ring_width - state.theme_settings.border_width) * focus; + (state.theme_settings.focus_ring_width - state.theme_settings.border_width)
* focus;
let border_color = state.theme_settings.border_color.0; let border_color = state.theme_settings.border_color.0;
let gradient = state.theme_settings.border_gradient; let gradient = state.theme_settings.border_gradient;
@@ -273,7 +304,10 @@ fn scene_elements(
scale, scale,
shadow_offset_y * offset_factor, shadow_offset_y * offset_factor,
shadow_blur * blur_factor, shadow_blur * blur_factor,
shadow_opacity * opacity_factor * f64::from(presentation_alpha) * decoration_progress, shadow_opacity
* opacity_factor
* f64::from(presentation_alpha)
* decoration_progress,
shadow_color, shadow_color,
output, output,
&programs, &programs,
@@ -313,9 +347,16 @@ fn scene_elements(
{ {
let mut color = state.theme_settings.surface_base_color.0; let mut color = state.theme_settings.surface_base_color.0;
color[3] = presentation_alpha; color[3] = presentation_alpha;
if let Some(fill) = if let Some(fill) = window_tint_element(
window_tint_element(&mut state.render, renderer, id, constrain, corners, color, true, output) &mut state.render,
{ renderer,
id,
constrain,
corners,
color,
true,
output,
) {
// Front-to-back: fill uncovered strips behind the native // Front-to-back: fill uncovered strips behind the native
// content instead of stretching it or exposing wallpaper. // content instead of stretching it or exposing wallpaper.
elements.push(fill.into()); elements.push(fill.into());
@@ -342,8 +383,13 @@ fn scene_elements(
elements.push(background); elements.push(background);
} }
if material_surface.is_none() if material_surface.is_none()
&& let Some(blur) = && let Some(blur) = super::window_blur::window_blur_element(
super::window_blur::window_blur_element(state, renderer, output, sample.presentation, corners) state,
renderer,
output,
sample.presentation,
corners,
)
{ {
elements.push(blur); elements.push(blur);
} }
@@ -420,20 +466,22 @@ pub(super) fn layer_elements(
requested_layers requested_layers
.iter() .iter()
.flat_map(|requested| { .flat_map(|requested| {
map.layers_on(*requested) map.layers_on(*requested).rev().filter_map(|layer| {
.rev() map.layer_geometry(layer)
.filter_map(|layer| map.layer_geometry(layer).map(|geometry| (geometry, layer.clone()))) .map(|geometry| (geometry, layer.clone()))
})
}) })
.collect::<Vec<_>>() .collect::<Vec<_>>()
}; };
state.render.surfaces.retain(|surface, _| { state.render.surfaces.retain(|surface, _| {
surface.is_alive() surface.is_alive()
|| state || state.render.closing.iter().any(|window| {
.render window
.closing .material
.iter() .as_ref()
.any(|window| window.material.as_ref().is_some_and(|(key, ..)| key == surface)) .is_some_and(|(key, ..)| key == surface)
})
}); });
let mut elements = Vec::new(); let mut elements = Vec::new();
@@ -486,7 +534,8 @@ pub(super) fn cursor_elements(
let Some(pointer) = state.seat.get_pointer() else { let Some(pointer) = state.seat.get_pointer() else {
return Vec::new(); return Vec::new();
}; };
let output_crop = Rectangle::<i32, Logical>::from_size(output_geometry.size).to_physical_precise_round(scale); let output_crop =
Rectangle::<i32, Logical>::from_size(output_geometry.size).to_physical_precise_round(scale);
let origin = Point::<i32, Physical>::default(); let origin = Point::<i32, Physical>::default();
let pointer_location = pointer.current_location() - output_geometry.loc.to_f64(); let pointer_location = pointer.current_location() - output_geometry.loc.to_f64();
match &state.cursor_status { match &state.cursor_status {
@@ -498,9 +547,13 @@ pub(super) fn cursor_elements(
.map(|attributes| attributes.lock().unwrap().hotspot) .map(|attributes| attributes.lock().unwrap().hotspot)
.unwrap_or_default() .unwrap_or_default()
}); });
let physical_location = (pointer_location - hotspot.to_f64()).to_physical_precise_round(scale); let physical_location =
(pointer_location - hotspot.to_f64()).to_physical_precise_round(scale);
render_elements_from_surface_tree::<GlesRenderer, WaylandSurfaceRenderElement<GlesRenderer>>( render_elements_from_surface_tree::<
GlesRenderer,
WaylandSurfaceRenderElement<GlesRenderer>,
>(
renderer, renderer,
surface, surface,
physical_location, physical_location,
@@ -511,7 +564,8 @@ pub(super) fn cursor_elements(
.into_iter() .into_iter()
.filter_map(|element| { .filter_map(|element| {
let element = RescaleRenderElement::from_element(element, origin, 1.0); let element = RescaleRenderElement::from_element(element, origin, 1.0);
let element = RelocateRenderElement::from_element(element, origin, Relocate::Relative); let element =
RelocateRenderElement::from_element(element, origin, Relocate::Relative);
CropRenderElement::from_element(element, scale, output_crop).map(Into::into) CropRenderElement::from_element(element, scale, output_crop).map(Into::into)
}) })
@@ -573,8 +627,9 @@ mod layer_crop_tests {
[1.0; 4], [1.0; 4],
RenderElementKind::Unspecified, RenderElementKind::Unspecified,
); );
let crop = CropRenderElement::from_element(panel, 1.0, layer_output_crop(mode, transform)) let crop =
.expect("bottom panel must remain visible"); CropRenderElement::from_element(panel, 1.0, layer_output_crop(mode, transform))
.expect("bottom panel must remain visible");
assert_eq!(crop.geometry(1.0.into()), geometry, "{transform:?}"); assert_eq!(crop.geometry(1.0.into()), geometry, "{transform:?}");
} }
} }
+49 -11
View File
@@ -3,7 +3,9 @@
use std::sync::Arc; use std::sync::Arc;
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement}; use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement};
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesPixelProgram, GlesRenderer, Uniform}; use smithay::backend::renderer::gles::{
GlesError, GlesFrame, GlesPixelProgram, GlesRenderer, Uniform,
};
use smithay::backend::renderer::utils::{CommitCounter, OpaqueRegions}; use smithay::backend::renderer::utils::{CommitCounter, OpaqueRegions};
use smithay::utils::{Buffer, Logical, Physical, Rectangle, Scale, Transform}; use smithay::utils::{Buffer, Logical, Physical, Rectangle, Scale, Transform};
@@ -47,7 +49,11 @@ impl SharedPixelShaderElement {
} }
} }
pub(crate) fn resize(&mut self, area: Rectangle<i32, Logical>, opaque: Option<Vec<Rectangle<i32, Logical>>>) { pub(crate) fn resize(
&mut self,
area: Rectangle<i32, Logical>,
opaque: Option<Vec<Rectangle<i32, Logical>>>,
) {
let opaque = opaque.unwrap_or_default(); let opaque = opaque.unwrap_or_default();
if self.state.area != area || self.state.opaque != opaque { if self.state.area != area || self.state.opaque != opaque {
@@ -75,7 +81,13 @@ impl Element for SharedPixelShaderElement {
} }
fn src(&self) -> Rectangle<f64, Buffer> { fn src(&self) -> Rectangle<f64, Buffer> {
Rectangle::from_size(self.state.area.size.to_f64().to_buffer(1.0, Transform::Normal)) Rectangle::from_size(
self.state
.area
.size
.to_f64()
.to_buffer(1.0, Transform::Normal),
)
} }
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> { fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
@@ -159,9 +171,13 @@ mod tests {
#[test] #[test]
#[ignore = "requires an EGL rendering device"] #[ignore = "requires an EGL rendering device"]
fn shared_shader_matches_smithay_and_keeps_old_frames_immutable() { fn shared_shader_matches_smithay_and_keeps_old_frames_immutable() {
let device = EGLDevice::enumerate().unwrap().last().expect("an EGL device"); let device = EGLDevice::enumerate()
.unwrap()
.last()
.expect("an EGL device");
let display = unsafe { EGLDisplay::new(device).unwrap() }; let display = unsafe { EGLDisplay::new(device).unwrap() };
let mut renderer = unsafe { GlesRenderer::new(EGLContext::new(&display).unwrap()).unwrap() }; let mut renderer =
unsafe { GlesRenderer::new(EGLContext::new(&display).unwrap()).unwrap() };
let program = renderer let program = renderer
.compile_custom_pixel_shader( .compile_custom_pixel_shader(
super::super::corner_shader(super::super::ROUNDED_SOLID_SHADER), super::super::corner_shader(super::super::ROUNDED_SOLID_SHADER),
@@ -191,7 +207,14 @@ mod tests {
uniforms(red), uniforms(red),
Kind::Unspecified, Kind::Unspecified,
); );
let mut shared = SharedPixelShaderElement::new(program, area, opaque, 0.5, uniforms(red), Kind::Unspecified); let mut shared = SharedPixelShaderElement::new(
program,
area,
opaque,
0.5,
uniforms(red),
Kind::Unspecified,
);
let old = shared.clone(); let old = shared.clone();
let old_commit = old.current_commit(); let old_commit = old.current_commit();
@@ -205,14 +228,20 @@ mod tests {
); );
for scale in [1.0, 1.25, 2.0] { for scale in [1.0, 1.25, 2.0] {
assert_eq!(shared.geometry(scale.into()), baseline.geometry(scale.into())); assert_eq!(
shared.geometry(scale.into()),
baseline.geometry(scale.into())
);
assert_eq!( assert_eq!(
&*shared.opaque_regions(scale.into()), &*shared.opaque_regions(scale.into()),
&*baseline.opaque_regions(scale.into()) &*baseline.opaque_regions(scale.into())
); );
} }
assert_eq!(pixels(&mut renderer, &shared), pixels(&mut renderer, &baseline)); assert_eq!(
pixels(&mut renderer, &shared),
pixels(&mut renderer, &baseline)
);
shared.resize(area, Some(vec![Rectangle::from_size((20, 20).into())])); shared.resize(area, Some(vec![Rectangle::from_size((20, 20).into())]));
assert!(Arc::ptr_eq(&shared.state, &old.state)); assert!(Arc::ptr_eq(&shared.state, &old.state));
shared.update_uniforms(uniforms(green)); shared.update_uniforms(uniforms(green));
@@ -220,12 +249,21 @@ mod tests {
assert_eq!(shared.id(), old.id()); assert_eq!(shared.id(), old.id());
assert_eq!(old.current_commit(), old_commit); assert_eq!(old.current_commit(), old_commit);
assert_ne!(shared.current_commit(), old_commit); assert_ne!(shared.current_commit(), old_commit);
assert_eq!(pixels(&mut renderer, &old), pixels(&mut renderer, &baseline)); assert_eq!(
assert_ne!(pixels(&mut renderer, &shared), pixels(&mut renderer, &baseline)); pixels(&mut renderer, &old),
pixels(&mut renderer, &baseline)
);
assert_ne!(
pixels(&mut renderer, &shared),
pixels(&mut renderer, &baseline)
);
let retained = shared.clone(); let retained = shared.clone();
shared.resize(Rectangle::from_size((16, 16).into()), None); shared.resize(Rectangle::from_size((16, 16).into()), None);
assert_eq!(retained.geometry(1.0.into()), area.to_physical_precise_round(1.0)); assert_eq!(
retained.geometry(1.0.into()),
area.to_physical_precise_round(1.0)
);
assert_eq!(shared.geometry(1.0.into()).size, (16, 16).into()); assert_eq!(shared.geometry(1.0.into()).size, (16, 16).into());
assert!(shared.opaque_regions(1.0.into()).is_empty()); assert!(shared.opaque_regions(1.0.into()).is_empty());
assert_eq!(shared.alpha(), baseline.alpha()); assert_eq!(shared.alpha(), baseline.alpha());
+62 -17
View File
@@ -13,14 +13,23 @@ fn text_element(
centered: bool, centered: bool,
) -> Option<AnimatedWindowRenderElement> { ) -> Option<AnimatedWindowRenderElement> {
let settings = state.tab_indicator_settings; let settings = state.tab_indicator_settings;
let color = settings.text_color.unwrap_or(state.theme_settings.text_primary_color.0); let color = settings
.text_color
.unwrap_or(state.theme_settings.text_primary_color.0);
let available = (rect.width - 16.0).max(0.0); let available = (rect.width - 16.0).max(0.0);
let mut title: String = text.chars().filter(|c| !c.is_control()).take(1024).collect(); let mut title: String = text
if state.indicator_text_width(&title, settings.font_size) > available || text.chars().count() > 1024 { .chars()
.filter(|c| !c.is_control())
.take(1024)
.collect();
if state.indicator_text_width(&title, settings.font_size) > available
|| text.chars().count() > 1024
{
let mut boundaries: Vec<_> = title.char_indices().map(|(index, _)| index).collect(); let mut boundaries: Vec<_> = title.char_indices().map(|(index, _)| index).collect();
boundaries.push(title.len()); boundaries.push(title.len());
let fits = boundaries.partition_point(|&end| { let fits = boundaries.partition_point(|&end| {
state.indicator_text_width(&format!("{}…", &title[..end]), settings.font_size) <= available state.indicator_text_width(&format!("{}…", &title[..end]), settings.font_size)
<= available
}); });
if fits == 0 { if fits == 0 {
title.clear(); title.clear();
@@ -37,14 +46,17 @@ fn text_element(
}; };
let location = Point::<i32, Physical>::from(( let location = Point::<i32, Physical>::from((
((rect.x - f64::from(output_origin.x) + x) * scale).round() as i32, ((rect.x - f64::from(output_origin.x) + x) * scale).round() as i32,
((rect.y - f64::from(output_origin.y) + rect.height * 0.5) * scale).round() as i32 - size.h / 2, ((rect.y - f64::from(output_origin.y) + rect.height * 0.5) * scale).round() as i32
- size.h / 2,
)); ));
let element = MemoryRenderBufferRenderElement::from_buffer( let element = MemoryRenderBufferRenderElement::from_buffer(
renderer, renderer,
location.to_f64(), location.to_f64(),
&buffer, &buffer,
Some(alpha), Some(alpha),
Some(Rectangle::from_size((f64::from(size.w), f64::from(size.h)).into())), Some(Rectangle::from_size(
(f64::from(size.w), f64::from(size.h)).into(),
)),
Some( Some(
( (
(f64::from(size.w) / scale).round() as i32, (f64::from(size.w) / scale).round() as i32,
@@ -57,12 +69,23 @@ fn text_element(
.ok()?; .ok()?;
let element = RescaleRenderElement::from_element(element, location, 1.0); let element = RescaleRenderElement::from_element(element, location, 1.0);
let element = RelocateRenderElement::from_element(element, (0, 0), Relocate::Relative); let element = RelocateRenderElement::from_element(element, (0, 0), Relocate::Relative);
let clip = Rect::new(rect.x + 8.0, rect.y, (rect.width - 16.0).max(0.0), rect.height); let clip = Rect::new(
CropRenderElement::from_element(element, scale, physical_rect(clip, output_origin, scale)).map(Into::into) rect.x + 8.0,
rect.y,
(rect.width - 16.0).max(0.0),
rect.height,
);
CropRenderElement::from_element(element, scale, physical_rect(clip, output_origin, scale))
.map(Into::into)
} }
fn title(state: &Ferese, id: WindowId, capture: bool) -> String { fn title(state: &Ferese, id: WindowId, capture: bool) -> String {
if capture && state.windows.window(id).is_none_or(|w| state.capture_protected(w)) { if capture
&& state
.windows
.window(id)
.is_none_or(|w| state.capture_protected(w))
{
return "Protected window".into(); return "Protected window".into();
} }
state state
@@ -99,8 +122,12 @@ pub(super) fn elements(
let scale = output.current_scale().fractional_scale(); let scale = output.current_scale().fractional_scale();
let settings = state.tab_indicator_settings; let settings = state.tab_indicator_settings;
let anchor = bar.group.windows[0]; let anchor = bar.group.windows[0];
let accent = settings.active_color.unwrap_or(state.theme_settings.accent_color.0); let accent = settings
let inactive = settings.inactive_color.unwrap_or(state.theme_settings.border_color.0); .active_color
.unwrap_or(state.theme_settings.accent_color.0);
let inactive = settings
.inactive_color
.unwrap_or(state.theme_settings.border_color.0);
let mut background = settings let mut background = settings
.background_color .background_color
.unwrap_or(state.theme_settings.surface_raised_color.0); .unwrap_or(state.theme_settings.surface_raised_color.0);
@@ -109,7 +136,8 @@ pub(super) fn elements(
state.theme_settings.material_style, state.theme_settings.material_style,
state.theme_settings.shell_opacity as f32, state.theme_settings.shell_opacity as f32,
); );
background[3] = if state.theme_settings.reduce_transparency || material.style == crate::config::MaterialStyle::Solid background[3] = if state.theme_settings.reduce_transparency
|| material.style == crate::config::MaterialStyle::Solid
{ {
1.0 1.0
} else { } else {
@@ -135,7 +163,15 @@ pub(super) fn elements(
.tab_indicator .tab_indicator
.first_row .first_row
.min(bar.group.windows.len().saturating_sub(g.rows)); .min(bar.group.windows.len().saturating_sub(g.rows));
for (row, id) in bar.group.windows.iter().copied().skip(first).take(g.rows).enumerate() { for (row, id) in bar
.group
.windows
.iter()
.copied()
.skip(first)
.take(g.rows)
.enumerate()
{
let rect = g.row(row); let rect = g.row(row);
let label = title(state, id, capture); let label = title(state, id, capture);
if let Some(element) = text_element( if let Some(element) = text_element(
@@ -153,7 +189,9 @@ pub(super) fn elements(
if id == bar.group.active { if id == bar.group.active {
// Match the control center's selected shell buttons: a quiet // Match the control center's selected shell buttons: a quiet
// foreground tint, with accent reserved for the active rail. // foreground tint, with accent reserved for the active rail.
let mut fill = settings.text_color.unwrap_or(state.theme_settings.text_primary_color.0); let mut fill = settings
.text_color
.unwrap_or(state.theme_settings.text_primary_color.0);
fill[3] = 0.14 * alpha * hover; fill[3] = 0.14 * alpha * hover;
if let Some(element) = overview_chrome_element( if let Some(element) = overview_chrome_element(
state, state,
@@ -172,7 +210,9 @@ pub(super) fn elements(
} }
} }
if settings.outline_width > 0.0 { if settings.outline_width > 0.0 {
let color = settings.outline_color.unwrap_or(state.theme_settings.border_color.0); let color = settings
.outline_color
.unwrap_or(state.theme_settings.border_color.0);
if let Some(element) = overview_chrome_element( if let Some(element) = overview_chrome_element(
state, state,
renderer, renderer,
@@ -224,7 +264,8 @@ pub(super) fn elements(
output_geometry.size.h as f64, output_geometry.size.h as f64,
); );
let rect = Rect::new( let rect = Rect::new(
(content.x + (content.width - width) * 0.5).clamp(bounds.x, bounds.x + bounds.width - width), (content.x + (content.width - width) * 0.5)
.clamp(bounds.x, bounds.x + bounds.width - width),
(content.y + settings.padding).clamp(bounds.y, bounds.y + bounds.height - g.row_height), (content.y + settings.padding).clamp(bounds.y, bounds.y + bounds.height - g.row_height),
width, width,
g.row_height, g.row_height,
@@ -263,7 +304,11 @@ pub(super) fn elements(
g.rail.width, g.rail.width,
g.segment_height, g.segment_height,
); );
let color = if id == bar.group.active { accent } else { inactive }; let color = if id == bar.group.active {
accent
} else {
inactive
};
if let Some(element) = overview_chrome_element( if let Some(element) = overview_chrome_element(
state, state,
renderer, renderer,
Loaded 100 of 336 files, more files were not shown because too many files have changed in this diff. Show more