Format Rust sources at 100 columns
This commit is contained in:
1 parent
4661f595f8
commit
610478f8c7
336 files changed
+22030
-6425
No files matched your search
@@ -48,10 +48,13 @@ impl Default for Engine {
|
||||
pomodoro: None,
|
||||
pomodoro_settings: Default::default(),
|
||||
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 {
|
||||
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,
|
||||
process_enabled: false,
|
||||
@@ -141,7 +144,9 @@ impl Engine {
|
||||
{
|
||||
match crate::activities_process::Watcher::start(self.rules.clone(), sender.clone()) {
|
||||
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()
|
||||
.chain(self.pomodoro.iter().map(|timer| timer.activity()))
|
||||
.collect(),
|
||||
pomodoro: self.pomodoro.as_ref().map(|timer| ferese_ipc::activities::Pomodoro {
|
||||
pomodoro: self
|
||||
.pomodoro
|
||||
.as_ref()
|
||||
.map(|timer| ferese_ipc::activities::Pomodoro {
|
||||
id: timer.id.clone(),
|
||||
timer: timer.session.clone(),
|
||||
}),
|
||||
windows: self
|
||||
.windows
|
||||
.iter()
|
||||
.map(|(activity, window)| ferese_ipc::activities::ActivityWindow {
|
||||
.map(
|
||||
|(activity, window)| ferese_ipc::activities::ActivityWindow {
|
||||
activity: activity.clone(),
|
||||
window: *window,
|
||||
})
|
||||
},
|
||||
)
|
||||
.collect(),
|
||||
});
|
||||
self.cached_snapshot = Some(snapshot.clone());
|
||||
@@ -206,7 +216,13 @@ impl Engine {
|
||||
pub fn get(&mut self, request: u64, sender: SyncSender<Response>) {
|
||||
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 {
|
||||
self.get(request, sender);
|
||||
} else if self.watchers.len() < 32 {
|
||||
@@ -258,7 +274,11 @@ impl Engine {
|
||||
self.store
|
||||
.next_deadline(now)
|
||||
.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(),
|
||||
) {
|
||||
tracing::error!(%error, "Activity deadline could not be armed");
|
||||
@@ -307,16 +327,17 @@ impl Engine {
|
||||
window: Option<u64>,
|
||||
) -> Result<serde_json::Value, &'static str> {
|
||||
let started = match &publish {
|
||||
Publish::Start { local, .. } | Publish::Run { local, .. } | Publish::Hook { local, .. } => {
|
||||
Some(local.clone())
|
||||
}
|
||||
Publish::Start { local, .. }
|
||||
| Publish::Run { local, .. }
|
||||
| Publish::Hook { local, .. } => Some(local.clone()),
|
||||
_ => None,
|
||||
};
|
||||
let now = ferese_ipc::activities::now_ms();
|
||||
if matches!(publish, Publish::Hook { .. }) && matched.is_none() {
|
||||
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) {
|
||||
return Err("owner publication quota exceeded");
|
||||
}
|
||||
@@ -392,7 +413,9 @@ impl Engine {
|
||||
value: Value::Indeterminate,
|
||||
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"})
|
||||
}
|
||||
}
|
||||
@@ -486,7 +509,10 @@ impl Engine {
|
||||
}
|
||||
|
||||
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;
|
||||
let mut ancestors = owner.ancestor_pids().peekable();
|
||||
ancestors.peek()?;
|
||||
@@ -495,7 +521,8 @@ impl crate::Ferese {
|
||||
.records()
|
||||
.filter_map(|(id, record)| {
|
||||
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))
|
||||
})
|
||||
.collect();
|
||||
@@ -546,15 +573,27 @@ mod tests {
|
||||
for since in [revision, engine.revision] {
|
||||
let (send, receive) = std::sync::mpsc::sync_channel(1);
|
||||
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);
|
||||
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);
|
||||
let (send, receive) = std::sync::mpsc::sync_channel(1);
|
||||
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]
|
||||
@@ -605,7 +644,10 @@ mod tests {
|
||||
fn snapshots_are_lazy_shared_and_immutable_across_publications() {
|
||||
let mut engine = Engine::default();
|
||||
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();
|
||||
assert!(Arc::ptr_eq(&before, &engine.snapshot()));
|
||||
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);
|
||||
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()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
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 matched = ferese_config::activities::Matcher::new(&Default::default())
|
||||
.unwrap()
|
||||
@@ -681,7 +729,10 @@ mod tests {
|
||||
#[test]
|
||||
fn terminal_matching_uses_nearest_unique_ancestor_and_never_guesses() {
|
||||
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!(
|
||||
origin_window(ancestry.into_iter(), &[(200, 7), (200, 9), (100, 8)]),
|
||||
None
|
||||
@@ -739,7 +790,9 @@ mod tests {
|
||||
.unwrap();
|
||||
engine.owner_exit(owner());
|
||||
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();
|
||||
assert!(engine.snapshot().windows.is_empty());
|
||||
}
|
||||
@@ -776,7 +829,14 @@ mod tests {
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
engine.store.entries().next().unwrap().data.output.as_deref(),
|
||||
engine
|
||||
.store
|
||||
.entries()
|
||||
.next()
|
||||
.unwrap()
|
||||
.data
|
||||
.output
|
||||
.as_deref(),
|
||||
Some("first")
|
||||
);
|
||||
engine
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
//! Activity deadlines share CLOCK_BOOTTIME with the published countdowns. A
|
||||
//! 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::{
|
||||
io,
|
||||
os::fd::{AsRawFd, FromRawFd, OwnedFd},
|
||||
@@ -13,9 +15,13 @@ pub(crate) struct Deadline {
|
||||
token: RegistrationToken,
|
||||
}
|
||||
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.
|
||||
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 {
|
||||
return Err(io::Error::last_os_error().into());
|
||||
}
|
||||
@@ -25,7 +31,8 @@ impl Deadline {
|
||||
|_, fd, state| {
|
||||
let mut expirations = 0_u64;
|
||||
// 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 {
|
||||
let error = io::Error::last_os_error();
|
||||
if error.kind() != io::ErrorKind::WouldBlock {
|
||||
@@ -50,7 +57,10 @@ impl Deadline {
|
||||
}
|
||||
let ms = deadline.unwrap_or(0);
|
||||
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 {
|
||||
tv_sec: (ms / 1_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() {
|
||||
let events = smithay::reexports::calloop::EventLoop::<crate::Ferese>::try_new().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!(
|
||||
info.lines()
|
||||
.any(|line| line.split_whitespace().collect::<Vec<_>>() == ["clockid:", "7"]),
|
||||
"{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 {
|
||||
fd: deadline.fd.as_raw_fd(),
|
||||
events: libc::POLLIN,
|
||||
|
||||
@@ -69,7 +69,9 @@ impl Engine {
|
||||
return Err("Pomodoro session changed; refresh its controls".into());
|
||||
}
|
||||
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();
|
||||
}
|
||||
match request.action {
|
||||
@@ -85,8 +87,8 @@ impl Engine {
|
||||
}
|
||||
let session = Session::start(self.pomodoro_settings.clone(), now)?;
|
||||
// Verify the long-lived host, never the short-lived CLI, as owner.
|
||||
let (owner, _pidfd, _) =
|
||||
crate::ipc::activity_owner::inspect(std::process::id()).map_err(|error| error.to_string())?;
|
||||
let (owner, _pidfd, _) = crate::ipc::activity_owner::inspect(std::process::id())
|
||||
.map_err(|error| error.to_string())?;
|
||||
let id = Id {
|
||||
owner,
|
||||
local: "pomodoro".into(),
|
||||
@@ -104,7 +106,10 @@ impl Engine {
|
||||
self.pomodoro = None;
|
||||
}
|
||||
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 {
|
||||
PomodoroAction::Pause => timer.session.pause(now)?,
|
||||
PomodoroAction::Resume => timer.session.resume(now)?,
|
||||
@@ -182,14 +187,24 @@ impl Alerts {
|
||||
let connection = zbus::blocking::connection::Builder::session()?
|
||||
.method_timeout(Duration::from_secs(2))
|
||||
.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));
|
||||
let reply = connection.call_method(
|
||||
Some("org.freedesktop.Notifications"),
|
||||
"/org/freedesktop/Notifications",
|
||||
Some("org.freedesktop.Notifications"),
|
||||
"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()?;
|
||||
Ok(())
|
||||
@@ -218,8 +233,7 @@ mod tests {
|
||||
}
|
||||
#[test]
|
||||
fn completion_uses_the_standard_notification_service_without_sound() {
|
||||
const TEST: &str =
|
||||
"activities::pomodoro::tests::completion_uses_the_standard_notification_service_without_sound";
|
||||
const TEST: &str = "activities::pomodoro::tests::completion_uses_the_standard_notification_service_without_sound";
|
||||
if !crate::startup_tests::private_runtime(TEST) {
|
||||
return;
|
||||
}
|
||||
@@ -296,7 +310,10 @@ mod tests {
|
||||
assert_eq!(first.id.owner.pid, std::process::id());
|
||||
assert!(control(&mut engine, PomodoroAction::Start).is_err());
|
||||
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().activities.len(), 1);
|
||||
control(&mut engine, PomodoroAction::Reset).unwrap();
|
||||
@@ -355,7 +372,10 @@ mod tests {
|
||||
engine.pomodoro_remove_output("test", None);
|
||||
assert!(engine.pomodoro_attach_output("new"));
|
||||
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]
|
||||
fn disabled_alerts_and_schema_errors_have_no_side_effects() {
|
||||
|
||||
@@ -26,7 +26,10 @@ impl Publisher {
|
||||
.as_ref()
|
||||
.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;
|
||||
// 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");
|
||||
if epoch != *current {
|
||||
locked(recipients);
|
||||
@@ -116,9 +124,16 @@ mod tests {
|
||||
|
||||
let publisher = Publisher::new().unwrap();
|
||||
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);
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -27,13 +27,20 @@ impl Watcher {
|
||||
std::thread::Builder::new()
|
||||
.name("ferese-activity-processes".into())
|
||||
.spawn(move || {
|
||||
let mut observed =
|
||||
BTreeMap::<Owner, (Arc<std::os::fd::OwnedFd>, ferese_config::activities::Matched)>::new();
|
||||
let mut observed = BTreeMap::<
|
||||
Owner,
|
||||
(
|
||||
Arc<std::os::fd::OwnedFd>,
|
||||
ferese_config::activities::Matched,
|
||||
),
|
||||
>::new();
|
||||
while !worker_stop.load(Ordering::Acquire) {
|
||||
let rules = rules.read().expect("activity rules").clone();
|
||||
let dead: Vec<_> = observed
|
||||
.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();
|
||||
for owner in dead {
|
||||
observed.remove(&owner);
|
||||
@@ -54,21 +61,30 @@ impl Watcher {
|
||||
if worker_stop.load(Ordering::Acquire) || observed.len() >= 128 {
|
||||
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 {
|
||||
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;
|
||||
};
|
||||
if observed.contains_key(&owner) || parents.iter().any(|parent| parent == "feresectl") {
|
||||
if observed.contains_key(&owner)
|
||||
|| parents.iter().any(|parent| parent == "feresectl")
|
||||
{
|
||||
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;
|
||||
};
|
||||
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;
|
||||
}
|
||||
let Ok(command) = String::from_utf8(bytes) else {
|
||||
@@ -79,14 +95,19 @@ impl Watcher {
|
||||
continue;
|
||||
};
|
||||
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;
|
||||
};
|
||||
let fd = Arc::new(fd);
|
||||
// Keep the inspected process identity alive through dispatch.
|
||||
// No raw command, cwd or environment crosses the dispatcher.
|
||||
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()
|
||||
{
|
||||
observed.insert(owner, (fd, matched));
|
||||
|
||||
+396
-127
@@ -113,13 +113,24 @@ impl DirectBackendState {
|
||||
self.active && !self.topology.reconciling && !self.lid.pending()
|
||||
}
|
||||
|
||||
pub(crate) fn process_dmabuf_imports(&mut self, imports: &mut crate::dmabuf_imports::ImportManager) {
|
||||
pub(crate) fn process_dmabuf_imports(
|
||||
&mut self,
|
||||
imports: &mut crate::dmabuf_imports::ImportManager,
|
||||
) {
|
||||
if !self.active || self.topology.reconciling {
|
||||
return;
|
||||
}
|
||||
|
||||
for device in self.devices.values_mut().filter(|device| device.drm.is_active()) {
|
||||
imports.process(device.dmabuf_global, &mut device.renderer, Some(device.render_node));
|
||||
for device in self
|
||||
.devices
|
||||
.values_mut()
|
||||
.filter(|device| device.drm.is_active())
|
||||
{
|
||||
imports.process(
|
||||
device.dmabuf_global,
|
||||
&mut device.renderer,
|
||||
Some(device.render_node),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,12 +158,14 @@ impl DirectBackendState {
|
||||
geometry: smithay::utils::Rectangle<i32, smithay::utils::Logical>,
|
||||
output: &Output,
|
||||
remaining: usize,
|
||||
) -> Result<Option<crate::render::ResizeSnapshot>, smithay::backend::renderer::gles::GlesError> {
|
||||
let Some(device) = self
|
||||
.devices
|
||||
.values_mut()
|
||||
.find(|device| device.outputs.values().any(|candidate| &candidate.output == output))
|
||||
else {
|
||||
) -> Result<Option<crate::render::ResizeSnapshot>, smithay::backend::renderer::gles::GlesError>
|
||||
{
|
||||
let Some(device) = self.devices.values_mut().find(|device| {
|
||||
device
|
||||
.outputs
|
||||
.values()
|
||||
.any(|candidate| &candidate.output == output)
|
||||
}) else {
|
||||
return Ok(None);
|
||||
};
|
||||
crate::render::capture_resize_snapshot(
|
||||
@@ -173,7 +186,12 @@ impl DirectBackendState {
|
||||
let device = self
|
||||
.devices
|
||||
.values_mut()
|
||||
.find(|device| device.outputs.values().any(|candidate| &candidate.output == output))
|
||||
.find(|device| {
|
||||
device
|
||||
.outputs
|
||||
.values()
|
||||
.any(|candidate| &candidate.output == output)
|
||||
})
|
||||
.ok_or("Window output is unavailable")?;
|
||||
crate::render::capture_window_buffer(
|
||||
&mut device.renderer,
|
||||
@@ -193,7 +211,12 @@ impl DirectBackendState {
|
||||
let device = self
|
||||
.devices
|
||||
.values_mut()
|
||||
.find(|device| device.outputs.values().any(|candidate| &candidate.output == output))
|
||||
.find(|device| {
|
||||
device
|
||||
.outputs
|
||||
.values()
|
||||
.any(|candidate| &candidate.output == output)
|
||||
})
|
||||
.ok_or("Window output is unavailable")?;
|
||||
crate::render::capture_window_frame(
|
||||
&mut device.renderer,
|
||||
@@ -314,7 +337,12 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
|
||||
let seat_name = session.seat();
|
||||
let udev_backend = UdevBackend::new(&seat_name)?;
|
||||
let primary_path = primary_gpu(&seat_name)?
|
||||
.or_else(|| udev_backend.device_list().next().map(|(_, path)| path.to_owned()))
|
||||
.or_else(|| {
|
||||
udev_backend
|
||||
.device_list()
|
||||
.next()
|
||||
.map(|(_, path)| path.to_owned())
|
||||
})
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "no DRM device found"))?;
|
||||
let mut libinput_context =
|
||||
Libinput::new_with_udev::<LibinputSessionInterface<LibSeatSession>>(session.clone().into());
|
||||
@@ -361,7 +389,10 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.topology
|
||||
.observe_devices(&[(primary_path.clone(), topology::device_identity(&primary_path))]);
|
||||
.observe_devices(&[(
|
||||
primary_path.clone(),
|
||||
topology::device_identity(&primary_path),
|
||||
)]);
|
||||
lid::request_refresh(state, false);
|
||||
|
||||
event_loop
|
||||
@@ -376,7 +407,9 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
|
||||
|
||||
InputEvent::DeviceRemoved { device } => {
|
||||
if let Some(backend) = state.direct_backend.as_mut() {
|
||||
backend.input_devices.retain(|candidate| candidate != &device);
|
||||
backend
|
||||
.input_devices
|
||||
.retain(|candidate| candidate != &device);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -451,7 +484,9 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
|
||||
tracing::info!("direct session reacquired; refreshing external state");
|
||||
}
|
||||
})?;
|
||||
event_loop.handle().insert_source(udev_backend, |event, _, state| {
|
||||
event_loop
|
||||
.handle()
|
||||
.insert_source(udev_backend, |event, _, state| {
|
||||
if let UdevEvent::Removed { device_id } = event
|
||||
&& let Some(node) = direct_node_for_device(state, device_id)
|
||||
&& let Some(backend) = state.direct_backend.as_mut()
|
||||
@@ -505,10 +540,12 @@ pub(crate) fn reload_input_devices(state: &mut Ferese) {
|
||||
|
||||
fn automatic_refresh_active(backend: &DirectBackendState) -> bool {
|
||||
backend.active
|
||||
&& backend
|
||||
.devices
|
||||
&& backend.devices.values().any(|device| {
|
||||
device
|
||||
.outputs
|
||||
.values()
|
||||
.any(|device| device.outputs.values().any(|output| output.settings.auto_refresh))
|
||||
.any(|output| output.settings.auto_refresh)
|
||||
})
|
||||
}
|
||||
|
||||
fn sync_battery_timer(state: &mut Ferese) {
|
||||
@@ -525,23 +562,28 @@ fn sync_battery_timer(state: &mut Ferese) {
|
||||
return;
|
||||
}
|
||||
|
||||
match state
|
||||
.loop_handle
|
||||
.insert_source(Timer::from_duration(Duration::from_secs(5)), |_, _, state| {
|
||||
match state.loop_handle.insert_source(
|
||||
Timer::from_duration(Duration::from_secs(5)),
|
||||
|_, _, state| {
|
||||
if let Some(backend) = state.direct_backend.as_mut() {
|
||||
backend.battery_timer = None;
|
||||
}
|
||||
update_power_policy(state);
|
||||
sync_battery_timer(state);
|
||||
TimeoutAction::Drop
|
||||
}) {
|
||||
},
|
||||
) {
|
||||
Ok(token) => backend.battery_timer = Some(token),
|
||||
Err(error) => tracing::warn!(%error, "could not arm automatic refresh battery timer"),
|
||||
}
|
||||
}
|
||||
|
||||
fn update_power_policy(state: &mut Ferese) {
|
||||
if !state.direct_backend.as_ref().is_some_and(automatic_refresh_active) {
|
||||
if !state
|
||||
.direct_backend
|
||||
.as_ref()
|
||||
.is_some_and(automatic_refresh_active)
|
||||
{
|
||||
return;
|
||||
}
|
||||
let Some(backend) = state.direct_backend.as_mut() else {
|
||||
@@ -575,7 +617,12 @@ pub(crate) fn set_output_profile(state: &mut Ferese, name: &str) -> Result<(), S
|
||||
if !backend.active || backend.lid.pending() {
|
||||
return Err("output session is inactive or refreshing lid state".into());
|
||||
}
|
||||
if name != "auto" && !state.output_profiles.iter().any(|profile| profile.name == name) {
|
||||
if name != "auto"
|
||||
&& !state
|
||||
.output_profiles
|
||||
.iter()
|
||||
.any(|profile| profile.name == name)
|
||||
{
|
||||
return Err(format!("unknown output profile {name:?}"));
|
||||
}
|
||||
let previous = backend.manual_outputs.clone();
|
||||
@@ -620,7 +667,10 @@ pub(crate) fn set_output_profile(state: &mut Ferese, name: &str) -> Result<(), S
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn set_output_layout(state: &mut Ferese, layout: crate::config::OutputLayout) -> Result<(), String> {
|
||||
pub(crate) fn set_output_layout(
|
||||
state: &mut Ferese,
|
||||
layout: crate::config::OutputLayout,
|
||||
) -> Result<(), String> {
|
||||
let backend = state
|
||||
.direct_backend
|
||||
.as_mut()
|
||||
@@ -636,7 +686,9 @@ pub(crate) fn set_output_layout(state: &mut Ferese, layout: crate::config::Outpu
|
||||
let available = match layout {
|
||||
crate::config::OutputLayout::InternalOnly => usable.iter().any(|monitor| monitor.internal),
|
||||
crate::config::OutputLayout::ExternalOnly => usable.iter().any(|monitor| !monitor.internal),
|
||||
crate::config::OutputLayout::Extend | crate::config::OutputLayout::Mirror => usable.len() >= 2,
|
||||
crate::config::OutputLayout::Extend | crate::config::OutputLayout::Mirror => {
|
||||
usable.len() >= 2
|
||||
}
|
||||
};
|
||||
if !available {
|
||||
return Err("this display mode requires monitors that are not connected".into());
|
||||
@@ -694,7 +746,11 @@ fn confirmation_baseline<T>(pending: Option<T>, previous: T) -> T {
|
||||
pending.unwrap_or(previous)
|
||||
}
|
||||
|
||||
fn arm_output_confirmation(state: &mut Ferese, previous: DesiredOutputConfiguration, manual: ManualOverride) {
|
||||
fn arm_output_confirmation(
|
||||
state: &mut Ferese,
|
||||
previous: DesiredOutputConfiguration,
|
||||
manual: ManualOverride,
|
||||
) {
|
||||
let backend = state.direct_backend.as_mut().unwrap();
|
||||
if let Some(token) = backend.confirmation_timer.take() {
|
||||
state.loop_handle.remove(token);
|
||||
@@ -703,7 +759,12 @@ fn arm_output_confirmation(state: &mut Ferese, previous: DesiredOutputConfigurat
|
||||
.desired_outputs
|
||||
.profile
|
||||
.as_ref()
|
||||
.and_then(|name| state.output_profiles.iter().find(|profile| &profile.name == name))
|
||||
.and_then(|name| {
|
||||
state
|
||||
.output_profiles
|
||||
.iter()
|
||||
.find(|profile| &profile.name == name)
|
||||
})
|
||||
.map_or(15, |profile| profile.confirm_timeout);
|
||||
let baseline = confirmation_baseline(backend.confirmation.take(), (previous, manual));
|
||||
if timeout == 0 || baseline.0.outputs.is_empty() {
|
||||
@@ -729,8 +790,14 @@ fn arm_output_confirmation(state: &mut Ferese, previous: DesiredOutputConfigurat
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn confirm_output_configuration(state: &mut Ferese, confirm: bool) -> Result<(), String> {
|
||||
let backend = state.direct_backend.as_mut().ok_or("direct backend unavailable")?;
|
||||
pub(crate) fn confirm_output_configuration(
|
||||
state: &mut Ferese,
|
||||
confirm: bool,
|
||||
) -> Result<(), String> {
|
||||
let backend = state
|
||||
.direct_backend
|
||||
.as_mut()
|
||||
.ok_or("direct backend unavailable")?;
|
||||
if let Some(token) = backend.confirmation_timer.take() {
|
||||
state.loop_handle.remove(token);
|
||||
}
|
||||
@@ -751,7 +818,10 @@ pub(crate) fn confirm_output_configuration(state: &mut Ferese, confirm: bool) ->
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn validate_live_outputs(state: &Ferese, profiles: &[OutputProfile]) -> Result<(), String> {
|
||||
pub(crate) fn validate_live_outputs(
|
||||
state: &Ferese,
|
||||
profiles: &[OutputProfile],
|
||||
) -> Result<(), String> {
|
||||
let Some(backend) = state.direct_backend.as_ref() else {
|
||||
return Ok(());
|
||||
};
|
||||
@@ -1018,11 +1088,19 @@ pub(crate) fn sleep_locked_outputs(state: &mut Ferese) {
|
||||
|
||||
// Apply runtime requests without changing output globals or workspace ownership.
|
||||
fn apply_output_power(state: &mut Ferese) {
|
||||
let Some(backend) = state.direct_backend.as_mut().filter(|backend| backend.can_render()) else {
|
||||
let Some(backend) = state
|
||||
.direct_backend
|
||||
.as_mut()
|
||||
.filter(|backend| backend.can_render())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
for device in backend.devices.values_mut().filter(|device| device.drm.is_active()) {
|
||||
for device in backend
|
||||
.devices
|
||||
.values_mut()
|
||||
.filter(|device| device.drm.is_active())
|
||||
{
|
||||
for output in device.outputs.values_mut() {
|
||||
if !output.power.needs_power_off() || output.frame_pending {
|
||||
continue;
|
||||
@@ -1057,15 +1135,16 @@ fn apply_output_power(state: &mut Ferese) {
|
||||
state.loop_handle.remove(token);
|
||||
}
|
||||
} else if backend.power_retry.is_none() {
|
||||
match state
|
||||
.loop_handle
|
||||
.insert_source(Timer::from_duration(Duration::from_secs(1)), |_, _, state| {
|
||||
match state.loop_handle.insert_source(
|
||||
Timer::from_duration(Duration::from_secs(1)),
|
||||
|_, _, state| {
|
||||
if let Some(backend) = state.direct_backend.as_mut() {
|
||||
backend.power_retry = None;
|
||||
}
|
||||
apply_output_power(state);
|
||||
TimeoutAction::Drop
|
||||
}) {
|
||||
},
|
||||
) {
|
||||
Ok(token) => backend.power_retry = Some(token),
|
||||
Err(error) => tracing::warn!(%error, "could not arm failed power-off retry"),
|
||||
}
|
||||
@@ -1121,10 +1200,9 @@ pub(crate) fn cursor_animation_visible(state: &Ferese) -> bool {
|
||||
if let Some(backend) = state.direct_backend.as_ref() {
|
||||
backend.can_render()
|
||||
&& backend.devices.values().any(|device| {
|
||||
device
|
||||
.outputs
|
||||
.values()
|
||||
.any(|output| output.power.can_render() && redraw::cursor_on_output(state, &output.output))
|
||||
device.outputs.values().any(|output| {
|
||||
output.power.can_render() && redraw::cursor_on_output(state, &output.output)
|
||||
})
|
||||
})
|
||||
} else {
|
||||
state
|
||||
@@ -1155,7 +1233,11 @@ pub(crate) fn render_surface(
|
||||
render_on(state, &outputs);
|
||||
}
|
||||
|
||||
fn redraw(state: &mut Ferese, selected: Option<&[Output]>, source: &'static std::panic::Location<'static>) {
|
||||
fn redraw(
|
||||
state: &mut Ferese,
|
||||
selected: Option<&[Output]>,
|
||||
source: &'static std::panic::Location<'static>,
|
||||
) {
|
||||
if let Some(transition) = state.desktop_transition.as_mut() {
|
||||
transition.redraw.extend(
|
||||
selected
|
||||
@@ -1190,7 +1272,11 @@ fn redraw(state: &mut Ferese, selected: Option<&[Output]>, source: &'static std:
|
||||
let affected = outputs
|
||||
.iter()
|
||||
.map(|(_, _, output)| {
|
||||
let scene = state.direct_backend.as_ref().unwrap().scene_output(state, output);
|
||||
let scene = state
|
||||
.direct_backend
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.scene_output(state, output);
|
||||
selected.is_none_or(|selected| selected.contains(output) || selected.contains(&scene))
|
||||
|| state.output_has_animations(&scene)
|
||||
|| state.output_has_pending_visual_changes(&scene)
|
||||
@@ -1205,12 +1291,12 @@ fn redraw(state: &mut Ferese, selected: Option<&[Output]>, source: &'static std:
|
||||
}
|
||||
}
|
||||
|
||||
state.direct_backend.as_mut().unwrap().scheduling_metrics.redraw(
|
||||
source,
|
||||
selected.is_none(),
|
||||
requested,
|
||||
outputs.len(),
|
||||
);
|
||||
state
|
||||
.direct_backend
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.scheduling_metrics
|
||||
.redraw(source, selected.is_none(), requested, outputs.len());
|
||||
arm_animation_timer(state);
|
||||
}
|
||||
|
||||
@@ -1236,7 +1322,11 @@ pub(crate) fn set_lid_closed(state: &mut Ferese, closed: bool) {
|
||||
}
|
||||
|
||||
pub(crate) fn system_resumed(state: &mut Ferese) {
|
||||
if state.direct_backend.as_ref().is_some_and(|backend| backend.active) {
|
||||
if state
|
||||
.direct_backend
|
||||
.as_ref()
|
||||
.is_some_and(|backend| backend.active)
|
||||
{
|
||||
let seat = state.seat.clone();
|
||||
state.idle_notifier_state.notify_activity(&seat);
|
||||
}
|
||||
@@ -1250,7 +1340,9 @@ pub(crate) fn reconcile_after_monitor_recovery(state: &mut Ferese) {
|
||||
fn internal_connector(interface: connector::Interface) -> bool {
|
||||
matches!(
|
||||
interface,
|
||||
connector::Interface::EmbeddedDisplayPort | connector::Interface::LVDS | connector::Interface::DSI
|
||||
connector::Interface::EmbeddedDisplayPort
|
||||
| connector::Interface::LVDS
|
||||
| connector::Interface::DSI
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1319,15 +1411,25 @@ fn render_output(
|
||||
let rendered = {
|
||||
(|| -> Result<bool, Box<dyn Error>> {
|
||||
let elements = if output.mirror_source.is_some() {
|
||||
mirror::elements(state, &mut device.renderer, &scene_output, &frame, &mut output)?
|
||||
mirror::elements(
|
||||
state,
|
||||
&mut device.renderer,
|
||||
&scene_output,
|
||||
&frame,
|
||||
&mut output,
|
||||
)?
|
||||
} else {
|
||||
sampled_output_elements(state, &mut device.renderer, &output.output, true, &frame)
|
||||
};
|
||||
let effects = frame_effect_metrics(&elements, output.output.current_scale().fractional_scale());
|
||||
let fullscreen = state.output_has_fullscreen_for_frame(&output.output, frame.overview.is_presenting());
|
||||
let effects =
|
||||
frame_effect_metrics(&elements, output.output.current_scale().fractional_scale());
|
||||
let fullscreen = state
|
||||
.output_has_fullscreen_for_frame(&output.output, frame.overview.is_presenting());
|
||||
let locked = state.session_lock.active();
|
||||
let flags = planes::frame_flags(fullscreen, frame.animating, locked);
|
||||
output.diagnostics.attempt(fullscreen, frame.animating, locked);
|
||||
output
|
||||
.diagnostics
|
||||
.attempt(fullscreen, frame.animating, locked);
|
||||
let result = output
|
||||
.surface
|
||||
.render_frame(
|
||||
@@ -1344,12 +1446,16 @@ fn render_output(
|
||||
|
||||
let primary_format = match &result.primary_element {
|
||||
PrimaryPlaneElement::Swapchain(primary) => Some(primary.buffer().format()),
|
||||
PrimaryPlaneElement::Element(element) => match element.underlying_storage(&mut device.renderer) {
|
||||
Some(UnderlyingStorage::Wayland(buffer)) => smithay::wayland::dmabuf::get_dmabuf(buffer)
|
||||
PrimaryPlaneElement::Element(element) => {
|
||||
match element.underlying_storage(&mut device.renderer) {
|
||||
Some(UnderlyingStorage::Wayland(buffer)) => {
|
||||
smithay::wayland::dmabuf::get_dmabuf(buffer)
|
||||
.ok()
|
||||
.map(|buffer| buffer.format()),
|
||||
.map(|buffer| buffer.format())
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
let feedback_candidates = feedback::candidates(
|
||||
&mut device.renderer,
|
||||
@@ -1361,7 +1467,10 @@ fn render_output(
|
||||
if result.needs_sync()
|
||||
&& let PrimaryPlaneElement::Swapchain(primary) = &result.primary_element
|
||||
{
|
||||
primary.sync.wait().inspect_err(|_| output.diagnostics.sync_failed())?;
|
||||
primary
|
||||
.sync
|
||||
.wait()
|
||||
.inspect_err(|_| output.diagnostics.sync_failed())?;
|
||||
}
|
||||
|
||||
if result.is_empty {
|
||||
@@ -1371,11 +1480,19 @@ fn render_output(
|
||||
.into_iter()
|
||||
.any(|cursor| state.has_pending_screencopy(&output.output, cursor))
|
||||
});
|
||||
output
|
||||
.dmabuf_feedback
|
||||
.observed_frame(primary_format, &result.states, feedback_candidates);
|
||||
output.dmabuf_feedback.observed_frame(
|
||||
primary_format,
|
||||
&result.states,
|
||||
feedback_candidates,
|
||||
);
|
||||
if output.mirror_source.is_some() {
|
||||
capture::capture_mirror(state, &mut device.renderer, &scene_output, &frame, &mut output);
|
||||
capture::capture_mirror(
|
||||
state,
|
||||
&mut device.renderer,
|
||||
&scene_output,
|
||||
&frame,
|
||||
&mut output,
|
||||
);
|
||||
} else {
|
||||
capture::capture_output(
|
||||
state,
|
||||
@@ -1447,16 +1564,23 @@ fn render_output(
|
||||
flags.contains(FrameFlags::ALLOW_PRIMARY_PLANE_SCANOUT),
|
||||
);
|
||||
capture::after_queue(queued, || {
|
||||
output
|
||||
.dmabuf_feedback
|
||||
.observed_frame(primary_format, &visibility, feedback_candidates);
|
||||
output.dmabuf_feedback.observed_frame(
|
||||
primary_format,
|
||||
&visibility,
|
||||
feedback_candidates,
|
||||
);
|
||||
if output.mirror_source.is_none() {
|
||||
state.display_presentation.queued(&output.output, &visibility);
|
||||
state
|
||||
.display_presentation
|
||||
.queued(&output.output, &visibility);
|
||||
}
|
||||
output.primary_commit = primary_commit;
|
||||
output
|
||||
.render_metrics
|
||||
.record_frame(render_started.elapsed(), &damage, missed_deadlines, effects);
|
||||
output.render_metrics.record_frame(
|
||||
render_started.elapsed(),
|
||||
&damage,
|
||||
missed_deadlines,
|
||||
effects,
|
||||
);
|
||||
output.frame_pending = true;
|
||||
if !recovery::arm_completion(&state.loop_handle, node, crtc, &mut output) {
|
||||
recovery::reconcile_device(state, node);
|
||||
@@ -1470,7 +1594,13 @@ fn render_output(
|
||||
});
|
||||
|
||||
if output.mirror_source.is_some() {
|
||||
capture::capture_mirror(state, &mut device.renderer, &scene_output, &frame, &mut output);
|
||||
capture::capture_mirror(
|
||||
state,
|
||||
&mut device.renderer,
|
||||
&scene_output,
|
||||
&frame,
|
||||
&mut output,
|
||||
);
|
||||
} else {
|
||||
capture::capture_output(
|
||||
state,
|
||||
@@ -1510,7 +1640,10 @@ fn render_output(
|
||||
|
||||
device.outputs.insert(crtc, output);
|
||||
restore_device(state, node, device);
|
||||
if !submitted && scene_output == state.direct_backend.as_ref().unwrap().devices[&node].outputs[&crtc].output {
|
||||
if !submitted
|
||||
&& scene_output
|
||||
== state.direct_backend.as_ref().unwrap().devices[&node].outputs[&crtc].output
|
||||
{
|
||||
schedule_frame_callbacks(state, node, crtc, plan.present_at);
|
||||
}
|
||||
|
||||
@@ -1534,13 +1667,22 @@ fn direct_node_for_device(state: &Ferese, device_id: libc::dev_t) -> Option<DrmN
|
||||
}
|
||||
|
||||
fn rescan_device(state: &mut Ferese, node: DrmNode) -> Result<bool, ()> {
|
||||
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 Err(());
|
||||
};
|
||||
let Some(mut device) = backend.devices.remove(&node) else {
|
||||
return Err(());
|
||||
};
|
||||
let scan = select_outputs(&device, &backend.monitors, &backend.desired_outputs, backend.low_power);
|
||||
let scan = select_outputs(
|
||||
&device,
|
||||
&backend.monitors,
|
||||
&backend.desired_outputs,
|
||||
backend.low_power,
|
||||
);
|
||||
let result = scan
|
||||
.map_err(|error| error.to_string())
|
||||
.and_then(|scan| transaction::apply_device(state, node, &mut device, scan));
|
||||
@@ -1556,7 +1698,12 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) -> Result<bool, ()> {
|
||||
fn refresh_connected_info(state: &mut Ferese) {
|
||||
let backend = state.direct_backend.as_mut().unwrap();
|
||||
for device in backend.devices.values_mut() {
|
||||
if let Ok(scan) = select_outputs(device, &backend.monitors, &backend.desired_outputs, backend.low_power) {
|
||||
if let Ok(scan) = select_outputs(
|
||||
device,
|
||||
&backend.monitors,
|
||||
&backend.desired_outputs,
|
||||
backend.low_power,
|
||||
) {
|
||||
device.connected_outputs = scan.connected_outputs;
|
||||
}
|
||||
transaction::update_applied_info(device);
|
||||
@@ -1570,7 +1717,10 @@ fn refresh_connected_info(state: &mut Ferese) {
|
||||
.flat_map(|device| device.connected_outputs.clone())
|
||||
.collect::<Vec<_>>();
|
||||
for previous in &backend.connected_outputs {
|
||||
if !inventory.iter().any(|output| output.identity == previous.identity) {
|
||||
if !inventory
|
||||
.iter()
|
||||
.any(|output| output.identity == previous.identity)
|
||||
{
|
||||
let mut missing = previous.clone();
|
||||
missing.connected = false;
|
||||
missing.enabled = false;
|
||||
@@ -1598,7 +1748,10 @@ fn remember_applied_configuration(state: &mut Ferese) {
|
||||
}
|
||||
let mut applied = backend.desired_outputs.clone();
|
||||
for desired in &mut applied.outputs {
|
||||
let monitor = backend.monitors.iter().find(|monitor| monitor.key == desired.key);
|
||||
let monitor = backend
|
||||
.monitors
|
||||
.iter()
|
||||
.find(|monitor| monitor.key == desired.key);
|
||||
let output = monitor.and_then(|monitor| {
|
||||
backend
|
||||
.devices
|
||||
@@ -1624,7 +1777,10 @@ fn remember_applied_configuration(state: &mut Ferese) {
|
||||
}
|
||||
|
||||
fn validate_desired_outputs(state: &Ferese) -> Result<(), String> {
|
||||
let backend = state.direct_backend.as_ref().ok_or("direct backend unavailable")?;
|
||||
let backend = state
|
||||
.direct_backend
|
||||
.as_ref()
|
||||
.ok_or("direct backend unavailable")?;
|
||||
validate_output_configuration(backend, &backend.desired_outputs).map(|_| ())
|
||||
}
|
||||
|
||||
@@ -1676,7 +1832,10 @@ fn validate_selected_outputs<'a>(
|
||||
) -> Result<(), String> {
|
||||
for output in outputs.into_iter().filter(|output| output.settings.enabled) {
|
||||
if !selected.contains(&output.key) {
|
||||
return Err(format!("enabled output {} has no usable CRTC assignment", output.key));
|
||||
return Err(format!(
|
||||
"enabled output {} has no usable CRTC assignment",
|
||||
output.key
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -1739,10 +1898,9 @@ fn animation_fallbacks(state: &mut Ferese) -> Vec<(DrmNode, crtc::Handle, Instan
|
||||
.into_iter()
|
||||
.flat_map(|backend| backend.devices.iter())
|
||||
.flat_map(|(node, device)| {
|
||||
device
|
||||
.outputs
|
||||
.iter()
|
||||
.map(move |(crtc, output)| (*node, *crtc, output.output.clone(), device.drm.is_active()))
|
||||
device.outputs.iter().map(move |(crtc, output)| {
|
||||
(*node, *crtc, output.output.clone(), device.drm.is_active())
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let mut fallbacks = Vec::new();
|
||||
@@ -1753,14 +1911,26 @@ fn animation_fallbacks(state: &mut Ferese) -> Vec<(DrmNode, crtc::Handle, Instan
|
||||
.direct_backend
|
||||
.as_ref()
|
||||
.is_some_and(|backend| backend.can_render());
|
||||
let scene = state.direct_backend.as_ref().unwrap().scene_output(state, &identity);
|
||||
let scene = state
|
||||
.direct_backend
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.scene_output(state, &identity);
|
||||
let animating = enabled && state.output_has_animations(&scene);
|
||||
let backend = state.direct_backend.as_mut().unwrap();
|
||||
let output = backend.devices.get_mut(&node).unwrap().outputs.get_mut(&crtc).unwrap();
|
||||
let output = backend
|
||||
.devices
|
||||
.get_mut(&node)
|
||||
.unwrap()
|
||||
.outputs
|
||||
.get_mut(&crtc)
|
||||
.unwrap();
|
||||
// Preserve recovery of a requested final frame if another output
|
||||
// advances this animation to rest before the watchdog is dispatched.
|
||||
let active = enabled
|
||||
&& (animating || (output.animation_fallback.waiting_for_frame() && output.scheduler.has_pending_request()));
|
||||
&& (animating
|
||||
|| (output.animation_fallback.waiting_for_frame()
|
||||
&& output.scheduler.has_pending_request()));
|
||||
let chained = output.render_timer.is_some() || output.frame_pending;
|
||||
if active && chained {
|
||||
backend.scheduling_metrics.chained_animation_observations += 1;
|
||||
@@ -1783,7 +1953,11 @@ fn animation_fallbacks(state: &mut Ferese) -> Vec<(DrmNode, crtc::Handle, Instan
|
||||
// from current output identities and waits so commit, replacement and teardown
|
||||
// cancel stale wakeups, including mirrored and sleeping outputs.
|
||||
fn resize_deadlines(state: &Ferese) -> Vec<(Output, Instant)> {
|
||||
let Some(backend) = state.direct_backend.as_ref().filter(|backend| backend.can_render()) else {
|
||||
let Some(backend) = state
|
||||
.direct_backend
|
||||
.as_ref()
|
||||
.filter(|backend| backend.can_render())
|
||||
else {
|
||||
return Vec::new();
|
||||
};
|
||||
if state.session_lock.sleeping {
|
||||
@@ -1809,7 +1983,11 @@ fn arm_animation_timer(state: &mut Ferese) {
|
||||
let deadline = animation_fallbacks(state)
|
||||
.iter()
|
||||
.map(|(_, _, deadline)| *deadline)
|
||||
.chain(resize_deadlines(state).into_iter().map(|(_, deadline)| deadline))
|
||||
.chain(
|
||||
resize_deadlines(state)
|
||||
.into_iter()
|
||||
.map(|(_, deadline)| deadline),
|
||||
)
|
||||
.min();
|
||||
let Some(backend) = state.direct_backend.as_mut() else {
|
||||
return;
|
||||
@@ -1887,7 +2065,12 @@ fn arm_animation_timer(state: &mut Ferese) {
|
||||
}
|
||||
}
|
||||
|
||||
fn schedule_frame_callbacks(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle, target: Duration) {
|
||||
fn schedule_frame_callbacks(
|
||||
state: &mut Ferese,
|
||||
node: DrmNode,
|
||||
crtc: crtc::Handle,
|
||||
target: Duration,
|
||||
) {
|
||||
if state.session_lock.sleeping {
|
||||
return;
|
||||
}
|
||||
@@ -1941,7 +2124,12 @@ fn schedule_frame_callbacks(state: &mut Ferese, node: DrmNode, crtc: crtc::Handl
|
||||
}
|
||||
}
|
||||
|
||||
fn deliver_frame_callbacks(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle, identity: &Output) {
|
||||
fn deliver_frame_callbacks(
|
||||
state: &mut Ferese,
|
||||
node: DrmNode,
|
||||
crtc: crtc::Handle,
|
||||
identity: &Output,
|
||||
) {
|
||||
let Some(backend) = state.direct_backend.as_mut() else {
|
||||
return;
|
||||
};
|
||||
@@ -2005,7 +2193,10 @@ fn send_frame_callbacks(state: &mut Ferese, output: &Output) {
|
||||
eligible.get(&surface.into()).cloned()
|
||||
});
|
||||
});
|
||||
let layers = layer_map_for_output(output).layers().cloned().collect::<Vec<_>>();
|
||||
let layers = layer_map_for_output(output)
|
||||
.layers()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
layers.iter().for_each(|layer| {
|
||||
layer.send_frame(output, state.start_time.elapsed(), None, |surface, _| {
|
||||
eligible.get(&surface.into()).cloned()
|
||||
@@ -2083,7 +2274,8 @@ fn select_outputs(
|
||||
)));
|
||||
}
|
||||
if let Some(mode) = selected_mode {
|
||||
let size = output_transform(settings.transform).transform_size(OutputMode::from(mode).size);
|
||||
let size =
|
||||
output_transform(settings.transform).transform_size(OutputMode::from(mode).size);
|
||||
let width = (size.w as f64 / settings.scale).round();
|
||||
let height = (size.h as f64 / settings.scale).round();
|
||||
if width < 1.0 || height < 1.0 || width > i32::MAX as f64 || height > i32::MAX as f64 {
|
||||
@@ -2114,7 +2306,12 @@ fn select_outputs(
|
||||
auto_refresh: settings.auto_refresh,
|
||||
physical_size: connector.size(),
|
||||
current_mode: selected_mode.map(connected_mode_info),
|
||||
available_modes: connector.modes().iter().copied().map(connected_mode_info).collect(),
|
||||
available_modes: connector
|
||||
.modes()
|
||||
.iter()
|
||||
.copied()
|
||||
.map(connected_mode_info)
|
||||
.collect(),
|
||||
scale: settings.scale,
|
||||
transform: settings.transform,
|
||||
configured_position: settings.position,
|
||||
@@ -2160,8 +2357,9 @@ fn select_outputs(
|
||||
));
|
||||
}
|
||||
|
||||
for ((connector, mode, settings, identity, mirror_source), crtc) in
|
||||
candidates.into_iter().zip(crtc_assignment::assign(&requests))
|
||||
for ((connector, mode, settings, identity, mirror_source), crtc) in candidates
|
||||
.into_iter()
|
||||
.zip(crtc_assignment::assign(&requests))
|
||||
{
|
||||
if let Some(crtc) = crtc {
|
||||
selections.push(OutputSelection {
|
||||
@@ -2183,7 +2381,10 @@ fn select_outputs(
|
||||
// Also guard the rescan immediately before applying a device transaction:
|
||||
// hotplug can invalidate the earlier configuration-wide validation.
|
||||
validate_selected_outputs(
|
||||
desired.outputs.iter().filter(|output| output.key.starts_with(&prefix)),
|
||||
desired
|
||||
.outputs
|
||||
.iter()
|
||||
.filter(|output| output.key.starts_with(&prefix)),
|
||||
&selected,
|
||||
)
|
||||
.map_err(io::Error::other)?;
|
||||
@@ -2205,7 +2406,10 @@ fn connected_mode_info(mode: DrmMode) -> ConnectedModeInfo {
|
||||
}
|
||||
}
|
||||
|
||||
fn select_mode(connector: &connector::Info, requested: Option<OutputModeRequest>) -> Option<DrmMode> {
|
||||
fn select_mode(
|
||||
connector: &connector::Info,
|
||||
requested: Option<OutputModeRequest>,
|
||||
) -> Option<DrmMode> {
|
||||
let fallback = || {
|
||||
connector
|
||||
.modes()
|
||||
@@ -2224,11 +2428,24 @@ fn select_mode(connector: &connector::Info, requested: Option<OutputModeRequest>
|
||||
.filter(|mode| mode.size() == (requested.width, requested.height));
|
||||
if let Some(refresh) = requested.refresh_millihertz {
|
||||
matching
|
||||
.min_by_key(|mode| OutputMode::from(**mode).refresh.unsigned_abs().abs_diff(refresh))
|
||||
.filter(|mode| OutputMode::from(**mode).refresh.unsigned_abs().abs_diff(refresh) <= 1_000)
|
||||
.min_by_key(|mode| {
|
||||
OutputMode::from(**mode)
|
||||
.refresh
|
||||
.unsigned_abs()
|
||||
.abs_diff(refresh)
|
||||
})
|
||||
.filter(|mode| {
|
||||
OutputMode::from(**mode)
|
||||
.refresh
|
||||
.unsigned_abs()
|
||||
.abs_diff(refresh)
|
||||
<= 1_000
|
||||
})
|
||||
.copied()
|
||||
} else {
|
||||
matching.max_by_key(|mode| OutputMode::from(**mode).refresh).copied()
|
||||
matching
|
||||
.max_by_key(|mode| OutputMode::from(**mode).refresh)
|
||||
.copied()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2249,7 +2466,10 @@ fn create_direct_output(
|
||||
mirror_source,
|
||||
} = selection;
|
||||
let drm_surface = drm.create_surface(crtc, mode, &[connector.handle()])?;
|
||||
let allocator = GbmAllocator::new(gbm.clone(), GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT);
|
||||
let allocator = GbmAllocator::new(
|
||||
gbm.clone(),
|
||||
GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT,
|
||||
);
|
||||
let node = DrmNode::from_file(gbm.as_fd())?;
|
||||
let render_node = node
|
||||
.node_with_type(NodeType::Render)
|
||||
@@ -2402,7 +2622,9 @@ fn output_transform(transform: OutputTransform) -> Transform {
|
||||
}
|
||||
|
||||
fn connector_edid(drm: &DrmDevice, connector: &connector::Info) -> Option<Vec<u8>> {
|
||||
drm.get_properties(connector.handle()).ok().and_then(|properties| {
|
||||
drm.get_properties(connector.handle())
|
||||
.ok()
|
||||
.and_then(|properties| {
|
||||
properties.iter().find_map(|(handle, raw)| {
|
||||
let info = drm.get_property(*handle).ok()?;
|
||||
if info.name().to_bytes() != b"EDID" {
|
||||
@@ -2437,11 +2659,11 @@ fn resolve_output_positions(
|
||||
) -> Result<(), String> {
|
||||
let mut placements = Vec::new();
|
||||
for device in backend.devices.values() {
|
||||
let scan =
|
||||
select_outputs(device, &backend.monitors, desired, backend.low_power).map_err(|error| error.to_string())?;
|
||||
let scan = select_outputs(device, &backend.monitors, desired, backend.low_power)
|
||||
.map_err(|error| error.to_string())?;
|
||||
for selection in scan.selections {
|
||||
let size =
|
||||
output_transform(selection.settings.transform).transform_size(OutputMode::from(selection.mode).size);
|
||||
let size = output_transform(selection.settings.transform)
|
||||
.transform_size(OutputMode::from(selection.mode).size);
|
||||
let key = format!("{}/{}", drm_device_key(&device.drm), selection.connector);
|
||||
let previous = device
|
||||
.outputs
|
||||
@@ -2466,13 +2688,22 @@ fn resolve_output_positions(
|
||||
}
|
||||
|
||||
fn refresh_monitor_policy(state: &mut Ferese) -> Result<(), String> {
|
||||
let backend = state.direct_backend.as_mut().ok_or("direct backend unavailable")?;
|
||||
let backend = state
|
||||
.direct_backend
|
||||
.as_mut()
|
||||
.ok_or("direct backend unavailable")?;
|
||||
let mut discovered = Vec::new();
|
||||
for device in backend.devices.values() {
|
||||
let drm = &device.drm;
|
||||
let key = drm_device_key(drm);
|
||||
for handle in drm.resource_handles().map_err(|error| error.to_string())?.connectors() {
|
||||
let connector = drm.get_connector(*handle, true).map_err(|error| error.to_string())?;
|
||||
for handle in drm
|
||||
.resource_handles()
|
||||
.map_err(|error| error.to_string())?
|
||||
.connectors()
|
||||
{
|
||||
let connector = drm
|
||||
.get_connector(*handle, true)
|
||||
.map_err(|error| error.to_string())?;
|
||||
if connector.state() != connector::State::Connected {
|
||||
continue;
|
||||
}
|
||||
@@ -2529,9 +2760,11 @@ fn refresh_monitor_policy(state: &mut Ferese) -> Result<(), String> {
|
||||
backend.manual_outputs.internal,
|
||||
backend.manual_outputs.layout,
|
||||
);
|
||||
backend
|
||||
.manual_outputs
|
||||
.observe(&backend.monitors, backend.lid.closed(), &state.output_profiles);
|
||||
backend.manual_outputs.observe(
|
||||
&backend.monitors,
|
||||
backend.lid.closed(),
|
||||
&state.output_profiles,
|
||||
);
|
||||
if previous_override
|
||||
!= (
|
||||
backend.manual_outputs.profile.clone(),
|
||||
@@ -2596,16 +2829,23 @@ impl PresentationClock {
|
||||
|
||||
fn set_refresh(&mut self, refresh_millihertz: i32) {
|
||||
if refresh_millihertz > 0 {
|
||||
self.refresh_interval = Some(Duration::from_nanos(1_000_000_000_000_u64 / refresh_millihertz as u64));
|
||||
self.refresh_interval = Some(Duration::from_nanos(
|
||||
1_000_000_000_000_u64 / refresh_millihertz as u64,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
fn record(&mut self, time: DrmEventTime, sequence: u32) -> Option<Duration> {
|
||||
let DrmEventTime::Monotonic(time) = time else {
|
||||
tracing::warn!(sequence, "DRM driver reported a realtime page-flip timestamp");
|
||||
tracing::warn!(
|
||||
sequence,
|
||||
"DRM driver reported a realtime page-flip timestamp"
|
||||
);
|
||||
return None;
|
||||
};
|
||||
let delta = self.last_presentation.map(|previous| time.saturating_sub(previous));
|
||||
let delta = self
|
||||
.last_presentation
|
||||
.map(|previous| time.saturating_sub(previous));
|
||||
|
||||
self.last_presentation = Some(time);
|
||||
self.presented_frames = self.presented_frames.saturating_add(1);
|
||||
@@ -2640,7 +2880,12 @@ mod tests {
|
||||
lid_closed: None,
|
||||
mirror_source: None,
|
||||
};
|
||||
crate::output_policy::select_profile(&monitors, false, &[profile], &ManualOverride::default())
|
||||
crate::output_policy::select_profile(
|
||||
&monitors,
|
||||
false,
|
||||
&[profile],
|
||||
&ManualOverride::default(),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2649,7 +2894,11 @@ mod tests {
|
||||
let selected = vec!["A".into(), "B".into(), "C".into()];
|
||||
assert!(validate_output_plan(&desired, &selected, || Ok(())).is_ok());
|
||||
assert_eq!(
|
||||
validate_output_plan(&desired, &selected, || Err("atomic TEST_ONLY failed".into())),
|
||||
validate_output_plan(
|
||||
&desired,
|
||||
&selected,
|
||||
|| Err("atomic TEST_ONLY failed".into())
|
||||
),
|
||||
Err("atomic TEST_ONLY failed".into())
|
||||
);
|
||||
}
|
||||
@@ -2658,7 +2907,8 @@ mod tests {
|
||||
fn requiredness_does_not_allow_a_connected_enabled_output_to_be_omitted() {
|
||||
let mut desired = three_monitor_plan(crate::config::OutputLayout::Extend);
|
||||
desired.outputs[2].settings.required = false;
|
||||
let error = validate_output_plan(&desired, &["A".into(), "B".into()], || Ok(())).unwrap_err();
|
||||
let error =
|
||||
validate_output_plan(&desired, &["A".into(), "B".into()], || Ok(())).unwrap_err();
|
||||
assert!(error.contains("C"));
|
||||
desired.outputs[2].settings.enabled = false;
|
||||
assert!(validate_output_plan(&desired, &["A".into(), "B".into()], || Ok(())).is_ok());
|
||||
@@ -2687,9 +2937,16 @@ mod tests {
|
||||
};
|
||||
for required in [true, false] {
|
||||
profile.outputs[2].required = required;
|
||||
let desired =
|
||||
crate::output_policy::select_profile(&connected, false, &[profile.clone()], &ManualOverride::default());
|
||||
assert_eq!(desired.profile.as_deref(), if required { None } else { Some("dock") });
|
||||
let desired = crate::output_policy::select_profile(
|
||||
&connected,
|
||||
false,
|
||||
&[profile.clone()],
|
||||
&ManualOverride::default(),
|
||||
);
|
||||
assert_eq!(
|
||||
desired.profile.as_deref(),
|
||||
if required { None } else { Some("dock") }
|
||||
);
|
||||
assert_eq!(desired.outputs.len(), 2);
|
||||
assert!(validate_output_plan(&desired, &["A".into(), "B".into()], || Ok(())).is_ok());
|
||||
}
|
||||
@@ -2740,7 +2997,8 @@ mod tests {
|
||||
#[test]
|
||||
fn feasible_three_monitor_assignment_passes_live_plan_validation() {
|
||||
let desired = three_monitor_plan(crate::config::OutputLayout::Extend);
|
||||
let requests = [&[1, 2][..], &[1, 3][..], &[1, 3][..]].map(|compatible| crtc_assignment::Candidates {
|
||||
let requests =
|
||||
[&[1, 2][..], &[1, 3][..], &[1, 3][..]].map(|compatible| crtc_assignment::Candidates {
|
||||
applied: None,
|
||||
kernel: None,
|
||||
compatible: compatible
|
||||
@@ -2761,7 +3019,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn live_plan_rejects_partial_extend_and_mirror_before_hardware_validation() {
|
||||
for layout in [crate::config::OutputLayout::Extend, crate::config::OutputLayout::Mirror] {
|
||||
for layout in [
|
||||
crate::config::OutputLayout::Extend,
|
||||
crate::config::OutputLayout::Mirror,
|
||||
] {
|
||||
let desired = three_monitor_plan(layout);
|
||||
for omitted in ["A", "B", "C"] {
|
||||
let selected = ["A", "B", "C"]
|
||||
@@ -2807,7 +3068,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn panel_detection_uses_connector_type_not_monitor_name() {
|
||||
assert!(internal_connector(connector::Interface::EmbeddedDisplayPort));
|
||||
assert!(internal_connector(
|
||||
connector::Interface::EmbeddedDisplayPort
|
||||
));
|
||||
assert!(internal_connector(connector::Interface::LVDS));
|
||||
assert!(internal_connector(connector::Interface::DSI));
|
||||
assert!(!internal_connector(connector::Interface::HDMIA));
|
||||
@@ -2840,7 +3103,10 @@ mod tests {
|
||||
fn presentation_clock_rejects_realtime_timestamps() {
|
||||
let mut clock = PresentationClock::default();
|
||||
|
||||
assert_eq!(clock.record(DrmEventTime::Realtime(SystemTime::now()), 1), None);
|
||||
assert_eq!(
|
||||
clock.record(DrmEventTime::Realtime(SystemTime::now()), 1),
|
||||
None
|
||||
);
|
||||
assert_eq!(clock.presented_frames, 0);
|
||||
}
|
||||
|
||||
@@ -2863,7 +3129,10 @@ mod tests {
|
||||
|
||||
clock.reset_timing();
|
||||
|
||||
assert_eq!(clock.record(DrmEventTime::Monotonic(Duration::from_secs(10)), 2), None);
|
||||
assert_eq!(
|
||||
clock.record(DrmEventTime::Monotonic(Duration::from_secs(10)), 2),
|
||||
None
|
||||
);
|
||||
assert_eq!(clock.presented_frames, 2);
|
||||
assert_eq!(clock.missed_deadlines, 0);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,8 @@ pub(crate) fn touch_cache(state: &mut Ferese) {
|
||||
if state.capture_last_used.replace(Instant::now()).is_some() {
|
||||
return;
|
||||
}
|
||||
if let Err(error) = state
|
||||
if let Err(error) =
|
||||
state
|
||||
.loop_handle
|
||||
.insert_source(Timer::from_duration(CACHE_IDLE), |_, _, state| {
|
||||
let Some(last_used) = state.capture_last_used else {
|
||||
@@ -85,9 +86,18 @@ pub(super) fn capture_mirror(
|
||||
}
|
||||
let elements = capture_output_elements(state, renderer, source, include_cursor, scene);
|
||||
// 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, || {
|
||||
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 {
|
||||
continue;
|
||||
};
|
||||
@@ -98,7 +108,12 @@ pub(super) fn capture_mirror(
|
||||
Default::default(),
|
||||
);
|
||||
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 {
|
||||
continue;
|
||||
};
|
||||
@@ -168,7 +183,10 @@ mod tests {
|
||||
#[test]
|
||||
#[ignore = "requires an EGL rendering device"]
|
||||
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 context = EGLContext::new(&display).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 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();
|
||||
let pixels = renderer.map_texture(&mapping).unwrap();
|
||||
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);
|
||||
|
||||
let target = renderer.bind(displayed.as_mut().unwrap()).unwrap();
|
||||
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();
|
||||
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(
|
||||
@@ -246,7 +278,9 @@ mod tests {
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -318,7 +352,10 @@ mod failure_tests {
|
||||
Ok::<(), ()>(())
|
||||
};
|
||||
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!(
|
||||
capture_request(&mut state, &output, false, || Err::<(), _>(
|
||||
"injected allocation failure"
|
||||
@@ -328,14 +365,20 @@ mod failure_tests {
|
||||
})
|
||||
.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[0].output, other_output);
|
||||
let outcome = receiver.try_recv().unwrap();
|
||||
assert_eq!(outcome.request, 41);
|
||||
assert_eq!(outcome.part, 0);
|
||||
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!(
|
||||
other_receiver.try_recv().is_err(),
|
||||
"other output's request must stay pending"
|
||||
|
||||
@@ -130,17 +130,31 @@ mod tests {
|
||||
#[test]
|
||||
fn every_connector_order_finds_a_complete_matching() {
|
||||
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 result = assign(&requests);
|
||||
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]
|
||||
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))]);
|
||||
}
|
||||
|
||||
@@ -159,18 +173,29 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
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| {
|
||||
requests
|
||||
.iter()
|
||||
.zip(assignment)
|
||||
.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
|
||||
})
|
||||
.count()
|
||||
});
|
||||
(assignment.iter().flatten().count(), retained[0], retained[1])
|
||||
(
|
||||
assignment.iter().flatten().count(),
|
||||
retained[0],
|
||||
retained[1],
|
||||
)
|
||||
}
|
||||
|
||||
fn enumerate(
|
||||
@@ -204,7 +229,8 @@ mod tests {
|
||||
.filter(|crtc| graph & (1 << (index * 3 + crtc - 1)) != 0)
|
||||
.collect::<Vec<_>>();
|
||||
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)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
@@ -213,13 +239,14 @@ mod tests {
|
||||
let actual = assign(&requests);
|
||||
let used = actual.iter().flatten().collect::<HashSet<_>>();
|
||||
assert_eq!(used.len(), actual.iter().flatten().count());
|
||||
assert!(
|
||||
requests
|
||||
.iter()
|
||||
.zip(&actual)
|
||||
.all(|(request, crtc)| crtc.is_none_or(|crtc| request.compatible.contains(&crtc)))
|
||||
assert!(requests.iter().zip(&actual).all(|(request, crtc)| {
|
||||
crtc.is_none_or(|crtc| request.compatible.contains(&crtc))
|
||||
}));
|
||||
assert_eq!(
|
||||
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.
|
||||
let internal = candidates(None, None, &[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 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]
|
||||
@@ -304,7 +337,10 @@ mod tests {
|
||||
#[test]
|
||||
fn restricted_connector_gets_its_only_free_crtc() {
|
||||
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))]
|
||||
);
|
||||
}
|
||||
@@ -312,7 +348,10 @@ mod tests {
|
||||
#[test]
|
||||
fn exhausted_crtcs_do_not_displace_surviving_output_or_assign_twice() {
|
||||
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))]
|
||||
);
|
||||
assert!(assign(&[]).is_empty());
|
||||
|
||||
@@ -54,7 +54,10 @@ pub(super) struct Diagnostics {
|
||||
|
||||
impl Diagnostics {
|
||||
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 {
|
||||
@@ -191,9 +194,18 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn primary_classification_does_not_infer_scanout_from_zero_damage() {
|
||||
assert_eq!(PrimaryOutcome::classify(true, false), PrimaryOutcome::DirectScanout);
|
||||
assert_eq!(PrimaryOutcome::classify(false, true), PrimaryOutcome::Composed);
|
||||
assert_eq!(PrimaryOutcome::classify(false, false), PrimaryOutcome::ReusedSwapchain);
|
||||
assert_eq!(
|
||||
PrimaryOutcome::classify(true, false),
|
||||
PrimaryOutcome::DirectScanout
|
||||
);
|
||||
assert_eq!(
|
||||
PrimaryOutcome::classify(false, true),
|
||||
PrimaryOutcome::Composed
|
||||
);
|
||||
assert_eq!(
|
||||
PrimaryOutcome::classify(false, false),
|
||||
PrimaryOutcome::ReusedSwapchain
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -224,7 +236,10 @@ mod tests {
|
||||
let mut d = Diagnostics::new("test".into(), true);
|
||||
let queued = Err::<(), _>("queue failed");
|
||||
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.scanout_submissions, 0);
|
||||
assert_eq!(d.counters.cursor_plane_submissions, 0);
|
||||
|
||||
@@ -72,17 +72,27 @@ impl OutputFeedback {
|
||||
// Restrict the preference to the observed primary format/modifier:
|
||||
// without ALLOW_PRIMARY_PLANE_SCANOUT_ANY Smithay compares the full
|
||||
// format with the swapchain. Renderer-only formats remain in fallback.
|
||||
match DmabufFeedbackBuilder::new(self.render_device, self.render_formats.iter().copied())
|
||||
match DmabufFeedbackBuilder::new(
|
||||
self.render_device,
|
||||
self.render_formats.iter().copied(),
|
||||
)
|
||||
.add_preference_tranche(self.scanout_device, TrancheFlags::Scanout, [format], 4..=6)
|
||||
.build()
|
||||
{
|
||||
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.visible = states
|
||||
.states
|
||||
@@ -203,12 +213,16 @@ pub(crate) fn refresh(state: &mut Ferese) {
|
||||
.filter(|id| {
|
||||
state
|
||||
.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(|window| window.toplevel())
|
||||
.filter(|toplevel| {
|
||||
with_renderer_surface_state(toplevel.wl_surface(), |state| state.buffer().is_some())
|
||||
with_renderer_surface_state(toplevel.wl_surface(), |state| {
|
||||
state.buffer().is_some()
|
||||
})
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.map(|toplevel| Id::from(toplevel.wl_surface()))
|
||||
@@ -251,8 +265,10 @@ mod tests {
|
||||
let feedback = DmabufFeedbackBuilder::new(10, [format(Fourcc::Argb8888, Modifier::Linear)])
|
||||
.build()
|
||||
.unwrap();
|
||||
let global = state.create_global_with_default_feedback::<Ferese>(&display.handle(), &feedback);
|
||||
let other = state.create_global_with_default_feedback::<Ferese>(&display.handle(), &feedback);
|
||||
let global =
|
||||
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)
|
||||
}
|
||||
fn visible(output: &mut OutputFeedback, id: &Id, f: Format) {
|
||||
@@ -273,11 +289,31 @@ mod tests {
|
||||
let linear = format(Fourcc::Argb8888, Modifier::Linear);
|
||||
let implicit = format(Fourcc::Argb8888, Modifier::Invalid);
|
||||
let opaque = format(Fourcc::Xrgb8888, Modifier::Linear);
|
||||
assert!(compatible(&formats(&[linear, implicit]), &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));
|
||||
assert!(compatible(
|
||||
&formats(&[linear, implicit]),
|
||||
&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]
|
||||
@@ -307,7 +343,11 @@ mod tests {
|
||||
let mut b = output(&[f], &[f]);
|
||||
visible(&mut a, &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 {
|
||||
global,
|
||||
feedback,
|
||||
@@ -320,7 +360,10 @@ mod tests {
|
||||
select(
|
||||
global,
|
||||
&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()
|
||||
);
|
||||
@@ -331,7 +374,10 @@ mod tests {
|
||||
select(
|
||||
global,
|
||||
&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()
|
||||
);
|
||||
@@ -347,7 +393,12 @@ mod tests {
|
||||
let mut o = output(&[f], &[f]);
|
||||
visible(&mut o, &id, f);
|
||||
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);
|
||||
assert!(o.preferred.is_none());
|
||||
visible(&mut o, &id, f);
|
||||
@@ -390,9 +441,12 @@ mod tests {
|
||||
"clipped storage must remain hidden"
|
||||
);
|
||||
let faded = solid(bounds, 0.8);
|
||||
assert!(!candidate_geometry(&faded, bounds, scale.into(), || panic!(
|
||||
"faded elements must not probe storage"
|
||||
)));
|
||||
assert!(!candidate_geometry(
|
||||
&faded,
|
||||
bounds,
|
||||
scale.into(),
|
||||
|| panic!("faded elements must not probe storage")
|
||||
));
|
||||
let mut partial = bounds;
|
||||
partial.size.w -= 1;
|
||||
assert!(!candidate_geometry(
|
||||
@@ -474,7 +528,9 @@ mod tests {
|
||||
let f = format(Fourcc::Argb8888, Modifier::Linear);
|
||||
let id = Id::new();
|
||||
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!(o.visible.is_empty() && o.preferred.is_none());
|
||||
super::super::capture::after_queue(Ok::<(), ()>(()), || visible(&mut o, &id, f)).unwrap();
|
||||
|
||||
@@ -24,8 +24,14 @@ struct Transition {
|
||||
// A single owner determines both the desired ramp and who loses control on failure.
|
||||
enum Source {
|
||||
Original,
|
||||
Client { control: ZwlrGammaControlV1, ramp: Ramp },
|
||||
NightLight { temperature: u16, ramp: Ramp },
|
||||
Client {
|
||||
control: ZwlrGammaControlV1,
|
||||
ramp: Ramp,
|
||||
},
|
||||
NightLight {
|
||||
temperature: u16,
|
||||
ramp: Ramp,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -136,7 +142,8 @@ impl Gamma {
|
||||
|
||||
fn configure(&mut self, settings: Settings, duration: Duration, now: Instant) {
|
||||
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;
|
||||
}
|
||||
self.replace_source(Source::NightLight {
|
||||
@@ -152,10 +159,13 @@ impl Gamma {
|
||||
Source::Original => &self.original,
|
||||
Source::NightLight { ramp, .. } | Source::Client { ramp, .. } => ramp,
|
||||
};
|
||||
self.transition = (!duration.is_zero() && !self.suspended && self.current != *target).then(|| Transition {
|
||||
self.transition =
|
||||
(!duration.is_zero() && !self.suspended && self.current != *target).then(|| {
|
||||
Transition {
|
||||
from: self.current.clone(),
|
||||
started: now,
|
||||
duration,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -181,8 +191,10 @@ impl Gamma {
|
||||
};
|
||||
let animated;
|
||||
let ramp = if let Some(transition) = &self.transition {
|
||||
let progress =
|
||||
now.saturating_duration_since(transition.started).as_secs_f64() / transition.duration.as_secs_f64();
|
||||
let progress = now
|
||||
.saturating_duration_since(transition.started)
|
||||
.as_secs_f64()
|
||||
/ transition.duration.as_secs_f64();
|
||||
if progress >= 1. {
|
||||
self.transition = None;
|
||||
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.
|
||||
sync_night_light(state);
|
||||
if state.night_light.enabled {
|
||||
return None;
|
||||
}
|
||||
let duration = state.animation_duration(TRANSITION_DURATION);
|
||||
let backend = state.direct_backend.as_mut().filter(|backend| backend.active)?;
|
||||
for device in backend.devices.values_mut().filter(|device| device.drm.is_active()) {
|
||||
let backend = state
|
||||
.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 {
|
||||
if &output.output != target || output.global.is_none() {
|
||||
continue;
|
||||
@@ -255,28 +278,41 @@ pub(crate) fn sync_night_light(state: &mut Ferese) {
|
||||
if !state.gamma_dirty {
|
||||
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;
|
||||
}
|
||||
state.gamma_dirty = false;
|
||||
if advance_night_light(state, true) && !state.gamma_timer_pending {
|
||||
let result = state
|
||||
.loop_handle
|
||||
.insert_source(Timer::from_duration(TRANSITION_INTERVAL), |_, _, state| {
|
||||
let result = state.loop_handle.insert_source(
|
||||
Timer::from_duration(TRANSITION_INTERVAL),
|
||||
|_, _, state| {
|
||||
if advance_night_light(state, false) {
|
||||
TimeoutAction::ToDuration(TRANSITION_INTERVAL)
|
||||
} else {
|
||||
state.gamma_timer_pending = false;
|
||||
TimeoutAction::Drop
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
match result {
|
||||
Ok(_) => state.gamma_timer_pending = true,
|
||||
Err(error) => {
|
||||
tracing::warn!(%error, "could not schedule night-light transition");
|
||||
if let Some(backend) = &mut state.direct_backend {
|
||||
for device in backend.devices.values_mut().filter(|device| device.drm.is_active()) {
|
||||
for output in device.outputs.values_mut().filter(|output| !output.power.needs_wake()) {
|
||||
for device in backend
|
||||
.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() {
|
||||
gamma.transition = None;
|
||||
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 {
|
||||
let duration = state.animation_duration(TRANSITION_DURATION);
|
||||
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;
|
||||
};
|
||||
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 {
|
||||
let ready = !output.power.needs_wake();
|
||||
let gamma = if ready {
|
||||
@@ -346,7 +390,11 @@ pub(crate) fn night_light_status(state: &Ferese) -> 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 backend = state.direct_backend.as_mut()?;
|
||||
for device in backend.devices.values_mut() {
|
||||
@@ -362,7 +410,11 @@ fn owned<'a>(state: &'a mut Ferese, target: &Output, owner: &ZwlrGammaControlV1)
|
||||
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())
|
||||
}
|
||||
|
||||
|
||||
@@ -182,9 +182,13 @@ impl Reader {
|
||||
"block",
|
||||
),
|
||||
)
|
||||
.and_then(|reply| reply.body().deserialize::<zbus::zvariant::OwnedFd>());
|
||||
.and_then(|reply| {
|
||||
reply.body().deserialize::<zbus::zvariant::OwnedFd>()
|
||||
});
|
||||
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"),
|
||||
}
|
||||
continue;
|
||||
@@ -302,16 +306,17 @@ fn request(state: &mut Ferese, reactivate: bool, wake: bool) {
|
||||
backend.lid_reader.lease.lock().unwrap().suspend = false;
|
||||
let probe = backend.lid.begin(reactivate, wake);
|
||||
let sent = backend.lid_reader.requests.send(probe).is_ok();
|
||||
let timer = state
|
||||
.loop_handle
|
||||
.insert_source(Timer::from_duration(Duration::from_secs(3)), move |_, _, state| {
|
||||
let timer = state.loop_handle.insert_source(
|
||||
Timer::from_duration(Duration::from_secs(3)),
|
||||
move |_, _, state| {
|
||||
if let Some(backend) = state.direct_backend.as_mut() {
|
||||
backend.lid_reader.deadline = None;
|
||||
}
|
||||
tracing::warn!("lid refresh deadline elapsed; retaining the latest observed state");
|
||||
complete_refresh(state, probe, None);
|
||||
TimeoutAction::Drop
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
if sent && let Ok(token) = timer {
|
||||
state.direct_backend.as_mut().unwrap().lid_reader.deadline = Some(token);
|
||||
|
||||
@@ -6,7 +6,8 @@ use smithay::utils::{Buffer, Physical, Rectangle};
|
||||
|
||||
/// Fit the complete source into the transformed target; black letterbox, never crop.
|
||||
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 height = (source.1 as f64 * scale).round().max(1.0) as i32;
|
||||
Rectangle::new(
|
||||
@@ -21,7 +22,10 @@ pub(super) fn elements(
|
||||
source: &Output,
|
||||
frame: &crate::state::FrameScene,
|
||||
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);
|
||||
// Target renderer owns this texture, including when source and target use different GPUs.
|
||||
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
|
||||
.mirror_texture
|
||||
.as_ref()
|
||||
@@ -44,7 +54,13 @@ pub(super) fn elements(
|
||||
.clone();
|
||||
target.mirror_commit.increment();
|
||||
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,7 +157,8 @@ mod tests {
|
||||
None,
|
||||
);
|
||||
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> =
|
||||
[(0, [1., 0., 0., 1.]), (32, [0., 0., 1., 1.])]
|
||||
.into_iter()
|
||||
.map(|(y, color)| {
|
||||
SolidColorRenderElement::new(
|
||||
@@ -175,9 +192,15 @@ mod tests {
|
||||
CommitCounter::default(),
|
||||
);
|
||||
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
|
||||
.copy_framebuffer(&framebuffer, Rectangle::from_size((96, 64).into()), Fourcc::Abgr8888)
|
||||
.copy_framebuffer(
|
||||
&framebuffer,
|
||||
Rectangle::from_size((96, 64).into()),
|
||||
Fourcc::Abgr8888,
|
||||
)
|
||||
.unwrap();
|
||||
let pixels = renderer.map_texture(&mapping).unwrap();
|
||||
let red = pixels
|
||||
|
||||
@@ -8,7 +8,8 @@ pub(super) fn inject(mode: &str) -> bool {
|
||||
use std::sync::LazyLock;
|
||||
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);
|
||||
MODE.as_deref() == Some(mode) && !USED.swap(true, Ordering::AcqRel)
|
||||
}
|
||||
@@ -57,7 +58,9 @@ pub(super) fn arm_completion(
|
||||
}
|
||||
|
||||
let identity = output.output.clone();
|
||||
match handle.insert_source(Timer::from_duration(COMPLETION_TIMEOUT), move |_, _, state| {
|
||||
match handle.insert_source(
|
||||
Timer::from_duration(COMPLETION_TIMEOUT),
|
||||
move |_, _, state| {
|
||||
let Some(output) = state
|
||||
.direct_backend
|
||||
.as_mut()
|
||||
@@ -74,14 +77,19 @@ pub(super) fn arm_completion(
|
||||
}
|
||||
|
||||
output.completion_timer = None;
|
||||
tracing::error!(?node, ?crtc, "DRM frame completion timed out; reconciling device");
|
||||
tracing::error!(
|
||||
?node,
|
||||
?crtc,
|
||||
"DRM frame completion timed out; reconciling device"
|
||||
);
|
||||
reconcile_device(state, node);
|
||||
} else {
|
||||
output.completion_timer = None;
|
||||
}
|
||||
|
||||
TimeoutAction::Drop
|
||||
}) {
|
||||
},
|
||||
) {
|
||||
Ok(token) => {
|
||||
output.completion_timer = Some(token);
|
||||
true
|
||||
@@ -106,7 +114,9 @@ pub(super) fn reconcile_device(state: &mut Ferese, node: DrmNode) {
|
||||
// sessions retain the invalidation for fresh enumeration on activation.
|
||||
let active = backend.active;
|
||||
if active {
|
||||
match state.loop_handle.insert_source(Timer::immediate(), |_, _, state| {
|
||||
match state
|
||||
.loop_handle
|
||||
.insert_source(Timer::immediate(), |_, _, state| {
|
||||
reconcile_outputs(state, false);
|
||||
TimeoutAction::Drop
|
||||
}) {
|
||||
@@ -125,10 +135,14 @@ mod tests {
|
||||
let now = Instant::now();
|
||||
let mut completion = Completion::default();
|
||||
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!(!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!(retirement::<(), &str>(Ok(None)), (None, false, None));
|
||||
assert_eq!(retirement::<(), &str>(Ok(Some(()))), (Some(()), true, None));
|
||||
@@ -148,7 +162,10 @@ mod tests {
|
||||
Some(Duration::from_millis(1))
|
||||
);
|
||||
// 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!(
|
||||
completion.remaining(now + COMPLETION_TIMEOUT + Duration::from_secs(1)),
|
||||
Some(Duration::ZERO)
|
||||
|
||||
@@ -13,7 +13,13 @@ impl AnimationFallback {
|
||||
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 {
|
||||
self.orphaned_since = 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 {
|
||||
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.requested_outputs += requested as u64;
|
||||
count.connected_outputs += connected as u64;
|
||||
@@ -119,7 +134,10 @@ pub(super) fn cursor_on_output(state: &crate::Ferese, output: &smithay::output::
|
||||
.map(|attributes| attributes.lock().unwrap().hotspot)
|
||||
.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(
|
||||
surface,
|
||||
(location - hotspot.to_f64()).to_i32_round(),
|
||||
)
|
||||
.to_f64()
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
let bounds = Rectangle::<f64, Logical>::new(
|
||||
(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)
|
||||
}
|
||||
@@ -152,7 +174,10 @@ mod tests {
|
||||
let mut idle_fast = AnimationFallback::default();
|
||||
let mut moving_slow = AnimationFallback::default();
|
||||
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!(
|
||||
moving_slow.deadline(now + slow, slow, true, false),
|
||||
Some(now + slow * 2)
|
||||
@@ -166,7 +191,10 @@ mod tests {
|
||||
let interval = Duration::from_millis(16);
|
||||
let mut output = AnimationFallback::default();
|
||||
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);
|
||||
assert_eq!(
|
||||
output.deadline(reconnected, interval, true, false),
|
||||
@@ -192,7 +220,10 @@ mod tests {
|
||||
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);
|
||||
@@ -210,9 +241,17 @@ mod tests {
|
||||
assert_eq!(fallback.deadline(start, interval, true, true), None);
|
||||
let plan = scheduler.begin().unwrap();
|
||||
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!(fallback.deadline(start + interval, interval, true, true), None);
|
||||
assert_eq!(
|
||||
fallback.deadline(start + interval, interval, true, true),
|
||||
None
|
||||
);
|
||||
scheduler.presented(interval);
|
||||
assert_eq!(
|
||||
fallback.deadline(start + interval, interval, true, false),
|
||||
@@ -220,7 +259,10 @@ mod tests {
|
||||
);
|
||||
scheduler.request(interval);
|
||||
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]
|
||||
@@ -237,13 +279,17 @@ mod tests {
|
||||
assert!(scheduler.has_pending_request());
|
||||
let deadline = fallback.deadline(start, interval, true, false).unwrap();
|
||||
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!(
|
||||
fallback.deadline(deadline, interval, needs_final_frame, false),
|
||||
Some(deadline)
|
||||
);
|
||||
// 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();
|
||||
assert!(!scheduler.has_pending_request());
|
||||
}
|
||||
@@ -296,7 +342,11 @@ mod tests {
|
||||
refresh,
|
||||
}),
|
||||
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()),
|
||||
);
|
||||
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[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() {
|
||||
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);
|
||||
|
||||
@@ -138,8 +138,15 @@ fn dispatch_frame(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle, identit
|
||||
arm_animation_timer(state);
|
||||
}
|
||||
|
||||
pub(super) fn record_frame_schedule(output: &mut DirectOutput, plan: FramePlan, cost: Duration, submitted: bool) {
|
||||
output.scheduler.rendered(plan, monotonic_now(), cost, submitted);
|
||||
pub(super) fn record_frame_schedule(
|
||||
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() {
|
||||
let started = monotonic_now().saturating_sub(cost);
|
||||
tracing::debug!(target: "ferese::render", output = %output.output.name(),
|
||||
|
||||
@@ -8,8 +8,14 @@ pub(super) struct SessionDevice<S: Session> {
|
||||
|
||||
impl<S: Session> SessionDevice<S> {
|
||||
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)?;
|
||||
Ok(Self { session, fd: Some(fd) })
|
||||
let fd = session.open(
|
||||
path,
|
||||
OFlags::RDWR | OFlags::CLOEXEC | OFlags::NOCTTY | OFlags::NONBLOCK,
|
||||
)?;
|
||||
Ok(Self {
|
||||
session,
|
||||
fd: Some(fd),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn device_fd(&self) -> io::Result<DeviceFd> {
|
||||
@@ -20,7 +26,10 @@ impl<S: Session> SessionDevice<S> {
|
||||
impl<S: Session> Drop for SessionDevice<S> {
|
||||
fn drop(&mut self) {
|
||||
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"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,11 +31,20 @@ impl<Node> Default for DeviceState<Node> {
|
||||
|
||||
pub(super) fn device_identity(path: &Path) -> Option<PathBuf> {
|
||||
// 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> {
|
||||
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
|
||||
.iter()
|
||||
.find(|(path, identity)| match &self.identity {
|
||||
@@ -48,7 +57,8 @@ impl<Node> DeviceState<Node> {
|
||||
pub fn failed(&mut self, now: Instant) {
|
||||
self.availability = Availability::Unavailable;
|
||||
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) {
|
||||
@@ -128,7 +138,8 @@ impl<Node: Copy + Eq + std::hash::Hash> Topology<Node> {
|
||||
) -> HashSet<Node> {
|
||||
let mut retired = HashSet::new();
|
||||
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)
|
||||
{
|
||||
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> {
|
||||
active
|
||||
.then(|| self.devices.values().filter_map(|device| device.retry_at).min())
|
||||
.then(|| {
|
||||
self.devices
|
||||
.values()
|
||||
.filter_map(|device| device.retry_at)
|
||||
.min()
|
||||
})
|
||||
.flatten()
|
||||
}
|
||||
}
|
||||
@@ -166,7 +182,10 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) {
|
||||
let Some(backend) = state.direct_backend.as_mut() else {
|
||||
return;
|
||||
};
|
||||
if !backend.topology.request(backend.active && !backend.lid.pending()) {
|
||||
if !backend
|
||||
.topology
|
||||
.request(backend.active && !backend.lid.pending())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -200,8 +219,11 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) {
|
||||
.output_profiles
|
||||
.iter()
|
||||
.any(|profile| profile.outputs.iter().any(|output| output.auto_refresh));
|
||||
backend.low_power =
|
||||
automatic && super::super::power::low_power(backend.low_power, super::super::power::battery_percent());
|
||||
backend.low_power = automatic
|
||||
&& super::super::power::low_power(
|
||||
backend.low_power,
|
||||
super::super::power::battery_percent(),
|
||||
);
|
||||
|
||||
if let Ok(observed) = &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));
|
||||
let existing = current_path
|
||||
.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 {
|
||||
Availability::Ready(node) | Availability::Degraded(node) => Some(node),
|
||||
_ => existing,
|
||||
@@ -269,7 +298,13 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) {
|
||||
|
||||
let mut failed_outputs = Vec::new();
|
||||
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(|()| {
|
||||
for (crtc, output) in &mut device.outputs {
|
||||
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));
|
||||
}
|
||||
|
||||
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(|()| {
|
||||
transaction::retire_disconnected(state);
|
||||
validate_desired_outputs(state)
|
||||
@@ -353,7 +393,10 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) {
|
||||
.outputs
|
||||
.iter()
|
||||
.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()))
|
||||
});
|
||||
let mut succeeded = Vec::new();
|
||||
@@ -380,7 +423,13 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
desktop_changes.layout = 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();
|
||||
backend.topology.reconciling = false;
|
||||
@@ -453,7 +504,8 @@ fn publish_runtime_desktop(state: &mut Ferese) -> crate::state::DesktopChanges {
|
||||
});
|
||||
let mode = OutputMode::from(mode);
|
||||
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));
|
||||
crate::state::DesktopOutput {
|
||||
output,
|
||||
@@ -472,7 +524,8 @@ fn publish_runtime_desktop(state: &mut Ferese) -> crate::state::DesktopChanges {
|
||||
for output in device.outputs.values_mut() {
|
||||
if output.mirror_source.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 {
|
||||
// Mirrors are physical scanouts, not workspace-owning outputs.
|
||||
@@ -483,7 +536,9 @@ fn publish_runtime_desktop(state: &mut Ferese) -> crate::state::DesktopChanges {
|
||||
None,
|
||||
);
|
||||
}
|
||||
output.surface.set_output_mode_source((&output.output).into());
|
||||
output
|
||||
.surface
|
||||
.set_output_mode_source((&output.output).into());
|
||||
}
|
||||
}
|
||||
changes
|
||||
@@ -542,7 +597,11 @@ mod tests {
|
||||
topology.devices.get_mut(&a).unwrap().ready(node);
|
||||
// A policy failure keeps the device open, so it must retain ownership
|
||||
// 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()))];
|
||||
topology.observe_devices(&observed);
|
||||
assert_eq!(
|
||||
@@ -551,9 +610,16 @@ mod tests {
|
||||
);
|
||||
assert_eq!(topology.devices[&a].availability, Availability::Missing);
|
||||
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);
|
||||
assert!(topology.retire_changed_devices(&observed, |_| Some(node)).is_empty());
|
||||
assert!(
|
||||
topology
|
||||
.retire_changed_devices(&observed, |_| Some(node))
|
||||
.is_empty()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -567,15 +633,28 @@ mod tests {
|
||||
let mut topology = Topology::<u64>::default();
|
||||
topology.observe_devices(&[(card0.clone(), Some(a.clone()))]);
|
||||
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 {
|
||||
topology.observe_devices(&observed);
|
||||
}
|
||||
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!(
|
||||
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])
|
||||
);
|
||||
}
|
||||
@@ -590,7 +669,10 @@ mod tests {
|
||||
topology.devices.get_mut(&card).unwrap().ready(node);
|
||||
topology.observe_devices(&[(card.clone(), Some(identity.clone()))]);
|
||||
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]
|
||||
@@ -608,7 +690,10 @@ mod tests {
|
||||
topology.retire_changed_devices(&unknown, |_| Some(1)),
|
||||
HashSet::from([1])
|
||||
);
|
||||
assert_eq!(topology.devices[&identity].availability, Availability::Missing);
|
||||
assert_eq!(
|
||||
topology.devices[&identity].availability,
|
||||
Availability::Missing
|
||||
);
|
||||
assert_eq!(
|
||||
topology.devices[&card].current_path(&card, &unknown),
|
||||
Some(card.as_path())
|
||||
@@ -624,9 +709,17 @@ mod tests {
|
||||
Some(card.as_path())
|
||||
);
|
||||
// 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);
|
||||
assert!(topology.retire_changed_devices(&known, |_| Some(1)).is_empty());
|
||||
assert!(
|
||||
topology
|
||||
.retire_changed_devices(&known, |_| Some(1))
|
||||
.is_empty()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -636,10 +729,16 @@ mod tests {
|
||||
let card0 = PathBuf::from("/dev/dri/card0");
|
||||
let card1 = PathBuf::from("/dev/dri/card1");
|
||||
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(&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));
|
||||
topology.observe_devices(&swapped);
|
||||
assert_eq!(
|
||||
@@ -650,7 +749,11 @@ mod tests {
|
||||
assert_eq!(topology.devices[&b].availability, Availability::Missing);
|
||||
topology.devices.get_mut(&a).unwrap().ready(1);
|
||||
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]
|
||||
@@ -693,12 +796,17 @@ mod tests {
|
||||
fn unavailable_devices_retry_independently_with_capped_backoff() {
|
||||
let now = Instant::now();
|
||||
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 mut second = DeviceState::default();
|
||||
second.ready(healthy);
|
||||
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);
|
||||
assert_eq!(device.retry_at, Some(now + Duration::from_secs(1)));
|
||||
for _ in 0..20 {
|
||||
@@ -710,8 +818,15 @@ mod tests {
|
||||
Availability::Ready(healthy)
|
||||
);
|
||||
assert!(topology.retry_deadline(false).is_none());
|
||||
assert_eq!(topology.retry_deadline(true), Some(now + Duration::from_secs(32)));
|
||||
topology.devices.get_mut(Path::new("/dev/dri/card0")).unwrap().missing();
|
||||
assert_eq!(
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,11 @@ use smithay::backend::allocator::Allocator;
|
||||
use smithay::backend::drm::exporter::{ExportBuffer, ExportFramebuffer};
|
||||
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() {
|
||||
if drm.get_property(*property)?.name().to_bytes() == name.as_bytes() {
|
||||
return Ok(*property);
|
||||
@@ -35,8 +39,9 @@ fn clear_secondary_planes(
|
||||
for plane in planes.cursor.iter().chain(&planes.overlay) {
|
||||
let properties = drm.get_properties(plane.handle)?;
|
||||
let owned = properties.iter().any(|(handle, value)| {
|
||||
drm.get_property(*handle)
|
||||
.is_ok_and(|info| info.name().to_bytes() == b"CRTC_ID" && *value == u32::from(crtc) as u64)
|
||||
drm.get_property(*handle).is_ok_and(|info| {
|
||||
info.name().to_bytes() == b"CRTC_ID" && *value == u32::from(crtc) as u64
|
||||
})
|
||||
});
|
||||
if owned {
|
||||
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| {
|
||||
device
|
||||
.outputs
|
||||
.get(&selection.crtc)
|
||||
.is_some_and(|output| output.connector == selection.connector.handle() && output.mode == selection.mode)
|
||||
device.outputs.get(&selection.crtc).is_some_and(|output| {
|
||||
output.connector == selection.connector.handle() && output.mode == selection.mode
|
||||
})
|
||||
}) && scan.selections.len() == device.outputs.len()
|
||||
{
|
||||
return Ok(());
|
||||
@@ -82,7 +89,10 @@ pub(super) fn validate_device(device: &DirectDevice, scan: &OutputScan) -> Resul
|
||||
};
|
||||
let mut buffers = 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 mut used_planes = HashSet::new();
|
||||
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
|
||||
)
|
||||
}) {
|
||||
if let Ok(buffer) = allocator.create_buffer(width.into(), height.into(), format.code, &[format.modifier])
|
||||
&& let Ok(Some(fb)) = exporter.add_framebuffer(drm.device_fd(), ExportBuffer::Allocator(&buffer), false)
|
||||
if let Ok(buffer) = allocator.create_buffer(
|
||||
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));
|
||||
break;
|
||||
}
|
||||
}
|
||||
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(drm, &mut request, primary, "CRTC_ID", u32::from(selection.crtc) as u64)?;
|
||||
add(
|
||||
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 [
|
||||
("SRC_X", 0),
|
||||
("SRC_Y", 0),
|
||||
@@ -176,7 +203,10 @@ pub(super) fn validate_device(device: &DirectDevice, scan: &OutputScan) -> Resul
|
||||
buffers.push(buffer);
|
||||
framebuffers.push(fb);
|
||||
}
|
||||
drm.atomic_commit(AtomicCommitFlags::TEST_ONLY | AtomicCommitFlags::ALLOW_MODESET, request)?;
|
||||
drm.atomic_commit(
|
||||
AtomicCommitFlags::TEST_ONLY | AtomicCommitFlags::ALLOW_MODESET,
|
||||
request,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -348,13 +378,19 @@ impl crate::output_transaction::HardwareTransaction for DeviceTransaction<'_> {
|
||||
let previous = self.device.outputs.get_mut(&crtc).unwrap();
|
||||
previous.surface = replacement.surface;
|
||||
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 {
|
||||
return Ok(());
|
||||
};
|
||||
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
|
||||
.surface
|
||||
.set_connectors(&[output.connector])
|
||||
@@ -368,9 +404,16 @@ impl crate::output_transaction::HardwareTransaction for DeviceTransaction<'_> {
|
||||
)
|
||||
.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.display_presentation.remove_output(&output.output);
|
||||
self.state
|
||||
.display_presentation
|
||||
.remove_output(&output.output);
|
||||
self.state
|
||||
.desktop_transition
|
||||
.as_mut()
|
||||
@@ -378,7 +421,9 @@ impl crate::output_transaction::HardwareTransaction for DeviceTransaction<'_> {
|
||||
.redraw
|
||||
.insert(output.output.clone());
|
||||
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);
|
||||
output.frame_pending = false;
|
||||
restored
|
||||
@@ -447,8 +492,12 @@ pub(super) fn apply_device(
|
||||
}
|
||||
for crtc in &changed {
|
||||
let selection = &selections[crtc];
|
||||
let mut output = staged.remove(crtc).or_else(|| device.outputs.remove(crtc)).unwrap();
|
||||
if output.connector != selection.connector.handle() || output.identity != selection.identity {
|
||||
let mut output = staged
|
||||
.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);
|
||||
let (handle, _) = create_output(
|
||||
state,
|
||||
@@ -482,7 +531,9 @@ pub(super) fn apply_device(
|
||||
state.display_presentation.remove_output(&output.output);
|
||||
}
|
||||
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);
|
||||
output.frame_pending = false;
|
||||
output.primary_commit = None;
|
||||
@@ -513,7 +564,11 @@ pub(super) fn update_applied_info(device: &mut DirectDevice) {
|
||||
for info in &mut device.connected_outputs {
|
||||
info.enabled = false;
|
||||
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.current_mode = Some(connected_mode_info(output.mode));
|
||||
info.scale = output.settings.scale;
|
||||
@@ -568,7 +623,12 @@ pub(super) fn recover_last_output(state: &mut Ferese) {
|
||||
.unwrap()
|
||||
.devices
|
||||
.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;
|
||||
}
|
||||
@@ -604,7 +664,11 @@ pub(super) fn recover_last_output(state: &mut Ferese) {
|
||||
let nodes = backend
|
||||
.devices
|
||||
.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)
|
||||
.collect::<Vec<_>>();
|
||||
if validate_desired_outputs(state).is_ok() {
|
||||
@@ -617,7 +681,12 @@ pub(super) fn recover_last_output(state: &mut Ferese) {
|
||||
.unwrap()
|
||||
.devices
|
||||
.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);
|
||||
state.direct_backend.as_mut().unwrap().output_error = original_error;
|
||||
|
||||
@@ -26,12 +26,16 @@ pub struct LaunchConfig {
|
||||
|
||||
impl LaunchConfig {
|
||||
pub fn from_environment() -> Result<Self, String> {
|
||||
let graphical_session = std::env::var_os("WAYLAND_DISPLAY").is_some() || std::env::var_os("DISPLAY").is_some();
|
||||
let graphical_session =
|
||||
std::env::var_os("WAYLAND_DISPLAY").is_some() || std::env::var_os("DISPLAY").is_some();
|
||||
|
||||
Self::parse(std::env::args_os().skip(1), graphical_session)
|
||||
}
|
||||
|
||||
fn parse(args: impl IntoIterator<Item = OsString>, graphical_session: bool) -> Result<Self, String> {
|
||||
fn parse(
|
||||
args: impl IntoIterator<Item = OsString>,
|
||||
graphical_session: bool,
|
||||
) -> Result<Self, String> {
|
||||
let mut args = args.into_iter().peekable();
|
||||
let mut requested_backend = None;
|
||||
let mut client = Vec::new();
|
||||
@@ -113,7 +117,10 @@ fn parse_backend(value: &OsString) -> Result<Option<BackendKind>, String> {
|
||||
Some("auto") => Ok(None),
|
||||
Some("nested") => Ok(Some(BackendKind::Nested)),
|
||||
Some("drm") => Ok(Some(BackendKind::Drm)),
|
||||
_ => Err(format!("unknown backend {:?}; expected auto, nested, or drm", value)),
|
||||
_ => Err(format!(
|
||||
"unknown backend {:?}; expected auto, nested, or drm",
|
||||
value
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,7 +155,11 @@ mod tests {
|
||||
.unwrap();
|
||||
assert!(config.supervise_client);
|
||||
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]
|
||||
@@ -194,25 +205,42 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(config.client, [OsString::from("ferese-effects-probe")]);
|
||||
assert!(config.client_capabilities.contains(ClientCapabilities::EFFECTS));
|
||||
assert!(
|
||||
config
|
||||
.client_capabilities
|
||||
.contains(ClientCapabilities::EFFECTS)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicitly_grants_shell_control_to_the_launched_private_client() {
|
||||
let config = LaunchConfig::parse(
|
||||
[OsString::from("--grant-shell-control"), OsString::from("ferese-shell")],
|
||||
[
|
||||
OsString::from("--grant-shell-control"),
|
||||
OsString::from("ferese-shell"),
|
||||
],
|
||||
true,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(config.client, [OsString::from("ferese-shell")]);
|
||||
assert!(config.client_capabilities.contains(ClientCapabilities::SHELL_CONTROL));
|
||||
assert!(!config.client_capabilities.contains(ClientCapabilities::EFFECTS));
|
||||
assert!(
|
||||
config
|
||||
.client_capabilities
|
||||
.contains(ClientCapabilities::SHELL_CONTROL)
|
||||
);
|
||||
assert!(
|
||||
!config
|
||||
.client_capabilities
|
||||
.contains(ClientCapabilities::EFFECTS)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_an_unknown_backend() {
|
||||
let error = LaunchConfig::parse([OsString::from("--backend"), OsString::from("other")], true).unwrap_err();
|
||||
let error =
|
||||
LaunchConfig::parse([OsString::from("--backend"), OsString::from("other")], true)
|
||||
.unwrap_err();
|
||||
|
||||
assert!(error.contains("unknown backend"));
|
||||
}
|
||||
|
||||
@@ -17,10 +17,10 @@ fn read_battery_percent(root: &Path) -> Option<u8> {
|
||||
.ok()
|
||||
.map(|text| text.trim().to_owned())
|
||||
};
|
||||
if entries
|
||||
.iter()
|
||||
.any(|path| read(path, "online").as_deref() == Some("1") && read(path, "type").as_deref() != Some("Battery"))
|
||||
{
|
||||
if entries.iter().any(|path| {
|
||||
read(path, "online").as_deref() == Some("1")
|
||||
&& read(path, "type").as_deref() != Some("Battery")
|
||||
}) {
|
||||
return None;
|
||||
}
|
||||
entries.iter().find_map(|path| {
|
||||
@@ -57,7 +57,9 @@ pub(super) fn low_refresh_mode(size: (u16, u16), modes: &[(u16, u16, i32)]) -> O
|
||||
modes
|
||||
.iter()
|
||||
.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))
|
||||
.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[..3]), Some(2));
|
||||
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]
|
||||
|
||||
+188
-71
@@ -26,7 +26,9 @@ use ferese_layout::{ColumnWidth, Direction, GapConfig, ViewportFocusStrategy};
|
||||
pub use input_motion::InputSettings;
|
||||
use input_motion::*;
|
||||
use outputs::*;
|
||||
pub use outputs::{LidPolicy, OutputLayout, OutputModeRequest, OutputProfile, OutputSettings, OutputTransform};
|
||||
pub use outputs::{
|
||||
LidPolicy, OutputLayout, OutputModeRequest, OutputProfile, OutputSettings, OutputTransform,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use smithay::input::keyboard::{Keycode, keysyms, xkb};
|
||||
pub(crate) use tab_indicator::TabIndicatorSettings;
|
||||
@@ -128,11 +130,19 @@ impl Config {
|
||||
ferese_panel::validate(panels).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)?;
|
||||
for daemon in &self.autostart {
|
||||
if daemon.command.first().is_none_or(|program| program.trim().is_empty()) {
|
||||
return Err(ConfigError::Binding("autostart command must contain a program".into()));
|
||||
if daemon
|
||||
.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()?;
|
||||
@@ -451,7 +461,10 @@ mod tests {
|
||||
let runtime = config.runtime_config().unwrap();
|
||||
assert_eq!(runtime.panel_background_opacity, Some(0.8));
|
||||
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());
|
||||
}
|
||||
|
||||
@@ -475,7 +488,12 @@ mod tests {
|
||||
#[test]
|
||||
fn validates_shell_composition_before_publishing_config() {
|
||||
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!(
|
||||
Config::parse_source(&source.replace("item \"clock\"", "item \"network\""))
|
||||
.unwrap()
|
||||
@@ -498,10 +516,14 @@ mod tests {
|
||||
"panel main {{ style {}; end {{ group status {{ item clock kind=clock; }}; }}; }}",
|
||||
style.key()
|
||||
);
|
||||
Config::parse_source(&source).unwrap().runtime_config().unwrap();
|
||||
Config::parse_source(&source)
|
||||
.unwrap()
|
||||
.runtime_config()
|
||||
.unwrap();
|
||||
}
|
||||
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("panel main { group-surface invalid; }").is_err());
|
||||
@@ -512,11 +534,18 @@ mod tests {
|
||||
#[test]
|
||||
fn group_padding_is_validated_before_live_publication() {
|
||||
for padding in ["0", "4.5", "12", "32"] {
|
||||
let source = format!("panel main {{ end {{ group status {{ island-padding {padding}; }}; }}; }}");
|
||||
Config::parse_source(&source).unwrap().runtime_config().unwrap();
|
||||
let source = format!(
|
||||
"panel main {{ end {{ group status {{ island-padding {padding}; }}; }}; }}"
|
||||
);
|
||||
Config::parse_source(&source)
|
||||
.unwrap()
|
||||
.runtime_config()
|
||||
.unwrap();
|
||||
}
|
||||
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!(
|
||||
Config::parse_source(&source)
|
||||
.map(|config| config.runtime_config().is_err())
|
||||
@@ -529,7 +558,8 @@ mod tests {
|
||||
fn rejects_removed_physics_keys_before_publication() {
|
||||
for property in ["spring", "viewport-spring"] {
|
||||
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();
|
||||
assert!(error.contains("unknown field"), "{error}");
|
||||
}
|
||||
@@ -541,10 +571,9 @@ mod tests {
|
||||
#[test]
|
||||
fn display_key_has_a_default_binding_and_can_be_rebound() {
|
||||
let bindings = parse("").runtime_config().unwrap().bindings;
|
||||
assert!(bindings.iter().any(
|
||||
|binding| binding.trigger == BindingTrigger::Keysym(keysyms::KEY_XF86Display)
|
||||
&& binding.action == BindingAction::ToggleDisplayMode
|
||||
));
|
||||
assert!(bindings.iter().any(|binding| binding.trigger
|
||||
== BindingTrigger::Keysym(keysyms::KEY_XF86Display)
|
||||
&& binding.action == BindingAction::ToggleDisplayMode));
|
||||
let bindings = parse(
|
||||
"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))
|
||||
);
|
||||
assert!(
|
||||
bindings
|
||||
.iter()
|
||||
.any(|binding| binding.modifiers.logo && binding.action == BindingAction::ToggleDisplayMode)
|
||||
bindings.iter().any(|binding| binding.modifiers.logo
|
||||
&& binding.action == BindingAction::ToggleDisplayMode)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -580,12 +608,9 @@ mod tests {
|
||||
.iter()
|
||||
.any(|binding| binding.trigger == BindingTrigger::Keysym(keysyms::KEY_F1))
|
||||
);
|
||||
assert!(
|
||||
bindings
|
||||
.iter()
|
||||
.any(|binding| binding.trigger == BindingTrigger::Keysym(keysyms::KEY_F2)
|
||||
&& binding.action == BindingAction::ToggleKeybindingGuide)
|
||||
);
|
||||
assert!(bindings.iter().any(|binding| binding.trigger
|
||||
== BindingTrigger::Keysym(keysyms::KEY_F2)
|
||||
&& binding.action == BindingAction::ToggleKeybindingGuide));
|
||||
}
|
||||
#[test]
|
||||
fn workspace_back_and_forth_config_is_optional_and_bindable() {
|
||||
@@ -635,7 +660,12 @@ mod tests {
|
||||
BindingAction::FocusMru(false),
|
||||
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 [
|
||||
("focus-last-window", BindingAction::FocusLastWindow),
|
||||
@@ -654,7 +684,8 @@ mod tests {
|
||||
false,
|
||||
true,
|
||||
false
|
||||
) && binding.action == BindingAction::FocusMru(false)));
|
||||
) && binding.action
|
||||
== BindingAction::FocusMru(false)));
|
||||
assert!(bindings.iter().any(|binding| binding.matches(
|
||||
23_u32.into(),
|
||||
&[keysyms::KEY_Tab],
|
||||
@@ -662,7 +693,8 @@ mod tests {
|
||||
false,
|
||||
true,
|
||||
true
|
||||
) && binding.action == BindingAction::FocusMru(true)));
|
||||
) && binding.action
|
||||
== BindingAction::FocusMru(true)));
|
||||
}
|
||||
|
||||
#[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 config = Config::parse_source(source).unwrap();
|
||||
config.runtime_config().unwrap();
|
||||
assert_eq!(config.input_settings().unwrap().touchpad.swipe_threshold, 96);
|
||||
assert_eq!(config.output_profiles().unwrap()[0].outputs[0].position, Some([0, 0]));
|
||||
assert_eq!(
|
||||
config.input_settings().unwrap().touchpad.swipe_threshold,
|
||||
96
|
||||
);
|
||||
assert_eq!(
|
||||
config.output_profiles().unwrap()[0].outputs[0].position,
|
||||
Some([0, 0])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -716,7 +754,9 @@ mod tests {
|
||||
);
|
||||
assert_eq!(config.xwayland, XwaylandConfig::default());
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -725,7 +765,10 @@ mod tests {
|
||||
let config = parse("");
|
||||
|
||||
assert_eq!(config.layout_mode(), LayoutMode::Scrolling);
|
||||
assert_eq!(config.default_column_width().unwrap(), ColumnWidth::Proportion(0.5));
|
||||
assert_eq!(
|
||||
config.default_column_width().unwrap(),
|
||||
ColumnWidth::Proportion(0.5)
|
||||
);
|
||||
assert_eq!(config.gap_config().unwrap(), GapConfig::default());
|
||||
}
|
||||
|
||||
@@ -735,7 +778,10 @@ mod tests {
|
||||
let numeric = parse("scrolling {\n default-column-width 1.0\n}\n");
|
||||
|
||||
assert_eq!(full.default_column_width().unwrap(), ColumnWidth::Full);
|
||||
assert_eq!(numeric.default_column_width().unwrap(), ColumnWidth::Proportion(1.0));
|
||||
assert_eq!(
|
||||
numeric.default_column_width().unwrap(),
|
||||
ColumnWidth::Proportion(1.0)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -759,10 +805,19 @@ mod tests {
|
||||
let minimal = parse("");
|
||||
let centered = parse("scrolling {\n focus-strategy \"center_on_focus\"\n}\n");
|
||||
let paged = parse("scrolling {\n focus-strategy \"paged\"\n}\n");
|
||||
assert_eq!(paged.scrolling_focus_strategy(), ViewportFocusStrategy::Paged);
|
||||
assert_eq!(
|
||||
paged.scrolling_focus_strategy(),
|
||||
ViewportFocusStrategy::Paged
|
||||
);
|
||||
|
||||
assert_eq!(minimal.scrolling_focus_strategy(), ViewportFocusStrategy::Minimal);
|
||||
assert_eq!(centered.scrolling_focus_strategy(), ViewportFocusStrategy::Center);
|
||||
assert_eq!(
|
||||
minimal.scrolling_focus_strategy(),
|
||||
ViewportFocusStrategy::Minimal
|
||||
);
|
||||
assert_eq!(
|
||||
centered.scrolling_focus_strategy(),
|
||||
ViewportFocusStrategy::Center
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -804,7 +859,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn parses_and_validates_layout_gaps() {
|
||||
let configured = parse("layout {\n inner-gap 6.0\n outer-gap 14.0\n smart-gaps #true\n}\n");
|
||||
let configured =
|
||||
parse("layout {\n inner-gap 6.0\n outer-gap 14.0\n smart-gaps #true\n}\n");
|
||||
let invalid = parse("layout {\n outer-gap -1.0\n}\n");
|
||||
|
||||
assert_eq!(
|
||||
@@ -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",
|
||||
);
|
||||
let invalid_color = parse("theme {\n colors {\n accent \"blue\"\n }\n}\n");
|
||||
let invalid_radius = parse("theme {\n geometry {\n window-radius -1.0\n }\n}\n");
|
||||
let invalid_opacity =
|
||||
parse("theme {\n shadow {\n soft {\n opacity 1.1\n }\n }\n}\n");
|
||||
let invalid_radius =
|
||||
parse("theme {\n geometry {\n window-radius -1.0\n }\n}\n");
|
||||
let invalid_opacity = parse(
|
||||
"theme {\n shadow {\n soft {\n opacity 1.1\n }\n }\n}\n",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
configured.theme_settings().unwrap(),
|
||||
@@ -877,15 +935,24 @@ mod tests {
|
||||
assert!(invalid_radius.theme_settings().is_err());
|
||||
assert!(invalid_opacity.theme_settings().is_err());
|
||||
assert!(
|
||||
ferese_config::from_str::<Config>("theme {\n material {\n style \"mist\"\n }\n}\n").is_err()
|
||||
ferese_config::from_str::<Config>(
|
||||
"theme {\n material {\n style \"mist\"\n }\n}\n"
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn materials_default_to_solid_and_reject_removed_style() {
|
||||
assert_eq!(parse("").theme_settings().unwrap().material_style, MaterialStyle::Solid);
|
||||
assert_eq!(
|
||||
parse("").theme_settings().unwrap().material_style,
|
||||
MaterialStyle::Solid
|
||||
);
|
||||
assert!(
|
||||
ferese_config::from_str::<Config>("theme {\n material {\n style \"glass\"\n }\n}\n").is_err()
|
||||
ferese_config::from_str::<Config>(
|
||||
"theme {\n material {\n style \"glass\"\n }\n}\n"
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -932,7 +999,9 @@ mod tests {
|
||||
"from \"#112233\"\nto \"#445566\"\nangle #inf\n",
|
||||
] {
|
||||
assert!(
|
||||
Config::parse_source(&format!("theme {{ focus-ring {{ gradient {{\n{settings}\n}} }} }}"))
|
||||
Config::parse_source(&format!(
|
||||
"theme {{ focus-ring {{ gradient {{\n{settings}\n}} }} }}"
|
||||
))
|
||||
.and_then(|config| config.theme_settings())
|
||||
.is_err()
|
||||
);
|
||||
@@ -1023,7 +1092,9 @@ mod tests {
|
||||
for key in ["active-opacity", "inactive-opacity", "inactive-dim"] {
|
||||
for value in ["-0.1", "1.1", "#nan", "#inf"] {
|
||||
assert!(
|
||||
Config::parse_source(&format!("appearance {{ focus-effect {{ {key} {value}; }} }}"))
|
||||
Config::parse_source(&format!(
|
||||
"appearance {{ focus-effect {{ {key} {value}; }} }}"
|
||||
))
|
||||
.and_then(|config| config.runtime_config())
|
||||
.is_err()
|
||||
);
|
||||
@@ -1031,7 +1102,9 @@ mod tests {
|
||||
}
|
||||
for value in ["-1", "#nan", "#inf"] {
|
||||
assert!(
|
||||
Config::parse_source(&format!("appearance {{ focus-effect {{ duration-ms {value}; }} }}"))
|
||||
Config::parse_source(&format!(
|
||||
"appearance {{ focus-effect {{ duration-ms {value}; }} }}"
|
||||
))
|
||||
.and_then(|config| config.runtime_config())
|
||||
.is_err()
|
||||
);
|
||||
@@ -1109,7 +1182,9 @@ mod tests {
|
||||
);
|
||||
for threshold in [0, 15, 1001] {
|
||||
assert!(
|
||||
parse(&format!("input {{ touchpad {{ swipe-threshold {threshold}; }} }}"))
|
||||
parse(&format!(
|
||||
"input {{ touchpad {{ swipe-threshold {threshold}; }} }}"
|
||||
))
|
||||
.input_settings()
|
||||
.is_err()
|
||||
);
|
||||
@@ -1133,8 +1208,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn guide_uses_effective_bindings_including_overrides_and_unbindings() {
|
||||
let config =
|
||||
parse("binding keys=\"Super+Q\" action=\"none\"\nbinding keys=\"Super+Tab\" action=\"toggle-floating\"\n");
|
||||
let config = parse(
|
||||
"binding keys=\"Super+Q\" action=\"none\"\nbinding keys=\"Super+Tab\" action=\"toggle-floating\"\n",
|
||||
);
|
||||
let input = config.input_settings().unwrap();
|
||||
let map = physical_keymap(&input).unwrap();
|
||||
let entries = config
|
||||
@@ -1143,12 +1219,13 @@ mod tests {
|
||||
.iter()
|
||||
.filter_map(|binding| binding.guide_entry(Some(&map)))
|
||||
.collect::<Vec<_>>();
|
||||
assert!(!entries.iter().any(|entry| entry["description"] == "Close window"));
|
||||
assert!(
|
||||
entries
|
||||
!entries
|
||||
.iter()
|
||||
.any(|entry| entry["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"));
|
||||
}
|
||||
|
||||
@@ -1157,7 +1234,11 @@ mod tests {
|
||||
let defaults = Config::default().runtime_config().unwrap();
|
||||
for (key, name, action) in [
|
||||
(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_XF86AudioPrev, "media-previous", "previous"),
|
||||
] {
|
||||
@@ -1186,7 +1267,9 @@ mod tests {
|
||||
parse("scrolling { default-column-display \"tabbed\"; }\n").default_column_display(),
|
||||
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]
|
||||
@@ -1224,7 +1307,11 @@ mod tests {
|
||||
&& binding.trigger == BindingTrigger::Keysym(keysyms::KEY_f)
|
||||
&& binding.action == action));
|
||||
}
|
||||
assert!(bindings.iter().any(|binding| binding.action == BindingAction::Exit));
|
||||
assert!(
|
||||
bindings
|
||||
.iter()
|
||||
.any(|binding| binding.action == BindingAction::Exit)
|
||||
);
|
||||
assert!(bindings.iter().any(|binding| binding.modifiers.logo
|
||||
&& !binding.modifiers.shift
|
||||
&& binding.trigger == BindingTrigger::Keysym(keysyms::KEY_space)
|
||||
@@ -1242,13 +1329,16 @@ mod tests {
|
||||
binding.modifiers.logo
|
||||
&& binding.modifiers.shift
|
||||
&& 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| {
|
||||
binding.trigger == BindingTrigger::Keysym(keysyms::KEY_Print)
|
||||
&& binding.modifiers == BindingModifiers::default()
|
||||
&& 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| {
|
||||
binding.modifiers.logo
|
||||
@@ -1269,18 +1359,26 @@ mod tests {
|
||||
assert_eq!(bindings.len(), 63);
|
||||
assert!(bindings.iter().any(|binding| {
|
||||
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 bindings = unbound.bindings(&input).unwrap();
|
||||
assert_eq!(bindings.len(), 62);
|
||||
assert!(!bindings.iter().any(|binding| binding.action == BindingAction::Close));
|
||||
assert!(
|
||||
!bindings
|
||||
.iter()
|
||||
.any(|binding| binding.action == BindingAction::Close)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accepts_physical_xkb_key_names() {
|
||||
let config = parse("binding keys=\"Super+AD06\" match=\"physical\" action=\"focus\" argument=\"left\"\n");
|
||||
let config = parse(
|
||||
"binding keys=\"Super+AD06\" match=\"physical\" action=\"focus\" argument=\"left\"\n",
|
||||
);
|
||||
let input = config.input_settings().unwrap();
|
||||
let bindings = config.bindings(&input).unwrap();
|
||||
|
||||
@@ -1292,9 +1390,13 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn rejects_duplicate_or_invalid_user_bindings() {
|
||||
let duplicate = parse("binding keys=\"Super+Q\" action=\"close\"\nbinding keys=\"logo+q\" action=\"close\"\n");
|
||||
let missing_command = parse("binding keys=\"Super+Enter\" action=\"spawn\" argument=\"missing\"\n");
|
||||
let invalid_argument = parse("binding keys=\"Super+Q\" action=\"close\" argument=\"left\"\n");
|
||||
let duplicate = parse(
|
||||
"binding keys=\"Super+Q\" action=\"close\"\nbinding keys=\"logo+q\" action=\"close\"\n",
|
||||
);
|
||||
let missing_command =
|
||||
parse("binding keys=\"Super+Enter\" action=\"spawn\" argument=\"missing\"\n");
|
||||
let invalid_argument =
|
||||
parse("binding keys=\"Super+Q\" action=\"close\" argument=\"left\"\n");
|
||||
|
||||
for config in [duplicate, missing_command, invalid_argument] {
|
||||
let input = config.input_settings().unwrap();
|
||||
@@ -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",
|
||||
);
|
||||
let rules = config.window_rules().unwrap();
|
||||
let result = window_rules::resolve(&rules, Some("org.example.editor.desktop"), Some("Document"), false);
|
||||
let result = window_rules::resolve(
|
||||
&rules,
|
||||
Some("org.example.editor.desktop"),
|
||||
Some("Document"),
|
||||
false,
|
||||
);
|
||||
|
||||
assert_eq!(result.workspace, Some(3));
|
||||
assert_eq!(result.floating, Some(true));
|
||||
@@ -1406,24 +1513,34 @@ mod tests {
|
||||
] {
|
||||
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]
|
||||
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);
|
||||
let manual = parse("output-profile laptop { output eDP-1 mode=\"2880x1800@60\"; }");
|
||||
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]
|
||||
fn rejects_invalid_output_profiles() {
|
||||
let invalid_scale = parse("output-profile name=\"bad\" {\n output match=\"eDP-1\" scale=0.0\n}\n");
|
||||
let invalid_mode = parse("output-profile name=\"bad\" {\n output match=\"eDP-1\" mode=\"native\"\n}\n");
|
||||
let duplicate =
|
||||
parse("output-profile name=\"bad\" {\n output match=\"eDP-1\"\n output match=\"eDP-1\"\n}\n");
|
||||
let invalid_scale =
|
||||
parse("output-profile name=\"bad\" {\n output match=\"eDP-1\" scale=0.0\n}\n");
|
||||
let invalid_mode =
|
||||
parse("output-profile name=\"bad\" {\n output match=\"eDP-1\" mode=\"native\"\n}\n");
|
||||
let duplicate = parse(
|
||||
"output-profile name=\"bad\" {\n output match=\"eDP-1\"\n output match=\"eDP-1\"\n}\n",
|
||||
);
|
||||
|
||||
assert!(invalid_scale.output_profiles().is_err());
|
||||
assert!(invalid_mode.output_profiles().is_err());
|
||||
|
||||
@@ -6,21 +6,39 @@ impl Config {
|
||||
Ok(FocusEffectSettings {
|
||||
enabled: effect.enabled,
|
||||
blur: effect.blur,
|
||||
active_opacity: unit_theme_value(effect.active_opacity, "appearance.focus_effect.active_opacity")?,
|
||||
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")?,
|
||||
active_opacity: unit_theme_value(
|
||||
effect.active_opacity,
|
||||
"appearance.focus_effect.active_opacity",
|
||||
)?,
|
||||
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> {
|
||||
let border_width = nonnegative_theme_value(self.theme.geometry.border_width, "geometry.border_width")?;
|
||||
let focus_ring_width =
|
||||
nonnegative_theme_value(self.theme.geometry.focus_ring_width, "geometry.focus_ring_width")?;
|
||||
let window_radius = nonnegative_theme_value(self.theme.geometry.window_radius, "geometry.window_radius")?;
|
||||
let shadow_offset_y = finite_theme_value(self.theme.shadow.soft.offset_y, "shadow.soft.offset_y")?;
|
||||
let border_width =
|
||||
nonnegative_theme_value(self.theme.geometry.border_width, "geometry.border_width")?;
|
||||
let focus_ring_width = nonnegative_theme_value(
|
||||
self.theme.geometry.focus_ring_width,
|
||||
"geometry.focus_ring_width",
|
||||
)?;
|
||||
let window_radius =
|
||||
nonnegative_theme_value(self.theme.geometry.window_radius, "geometry.window_radius")?;
|
||||
let shadow_offset_y =
|
||||
finite_theme_value(self.theme.shadow.soft.offset_y, "shadow.soft.offset_y")?;
|
||||
let shadow_blur = nonnegative_theme_value(self.theme.shadow.soft.blur, "shadow.soft.blur")?;
|
||||
let shadow_opacity = unit_theme_value(self.theme.shadow.soft.opacity, "shadow.soft.opacity")?;
|
||||
let shadow_opacity =
|
||||
unit_theme_value(self.theme.shadow.soft.opacity, "shadow.soft.opacity")?;
|
||||
|
||||
let material_radius = nonnegative_theme_value(
|
||||
self.theme.geometry.shell_radius.unwrap_or(14.0),
|
||||
@@ -30,13 +48,22 @@ impl Config {
|
||||
border_width,
|
||||
focus_ring_width,
|
||||
border_color: parse_color(&self.theme.colors.border, "colors.border")?,
|
||||
text_primary_color: parse_color(&self.theme.colors.text_primary, "colors.text_primary")?,
|
||||
text_primary_color: parse_color(
|
||||
&self.theme.colors.text_primary,
|
||||
"colors.text_primary",
|
||||
)?,
|
||||
accent_color: parse_color(&self.theme.colors.accent, "colors.accent")?,
|
||||
border_gradient: self.theme.border.settings("border")?,
|
||||
focus_ring_gradient: self.theme.focus_ring.settings("focus_ring")?,
|
||||
shadow_color: parse_color(&self.theme.colors.shadow, "colors.shadow")?,
|
||||
surface_base_color: parse_color(&self.theme.colors.surface_base, "colors.surface_base")?,
|
||||
surface_raised_color: parse_color(&self.theme.colors.surface_raised, "colors.surface_raised")?,
|
||||
surface_base_color: parse_color(
|
||||
&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(
|
||||
self.theme
|
||||
.surface
|
||||
@@ -52,8 +79,15 @@ impl Config {
|
||||
shadow_blur,
|
||||
shadow_opacity,
|
||||
material_style: self.theme.material.style,
|
||||
backdrop_blur: nonnegative_theme_value(self.theme.material.blur_radius, "material.blur_radius")?.min(32.0),
|
||||
material_tint_strength: unit_theme_value(self.theme.material.tint_strength, "material.tint_strength")?,
|
||||
backdrop_blur: nonnegative_theme_value(
|
||||
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,
|
||||
panel_radius: material_radius,
|
||||
reduce_transparency: false,
|
||||
@@ -189,7 +223,11 @@ impl BorderPaintConfig {
|
||||
"focus_ring.gradient.angle",
|
||||
)
|
||||
} else {
|
||||
("border.gradient.from", "border.gradient.to", "border.gradient.angle")
|
||||
(
|
||||
"border.gradient.from",
|
||||
"border.gradient.to",
|
||||
"border.gradient.angle",
|
||||
)
|
||||
};
|
||||
let gradient = self
|
||||
.gradient
|
||||
|
||||
@@ -12,7 +12,10 @@ impl Config {
|
||||
fn binding_commands(&self) -> HashMap<String, Vec<String>> {
|
||||
let mut commands = HashMap::from([
|
||||
("terminal".to_owned(), vec!["foot".to_owned()]),
|
||||
("screenshot".to_owned(), vec!["ferese-screenshot".to_owned()]),
|
||||
(
|
||||
"screenshot".to_owned(),
|
||||
vec!["ferese-screenshot".to_owned()],
|
||||
),
|
||||
(
|
||||
"screenshot-full".to_owned(),
|
||||
vec!["ferese-screenshot".to_owned(), "--full".to_owned()],
|
||||
@@ -36,7 +39,10 @@ impl Config {
|
||||
for configured in &self.bindings {
|
||||
let identity = binding_identity(configured, &keymap)?;
|
||||
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);
|
||||
@@ -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 {
|
||||
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() {
|
||||
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 action = configured
|
||||
.action
|
||||
.as_deref()
|
||||
.ok_or_else(|| ConfigError::Binding(format!("binding {:?} has no action", configured.keys)))?;
|
||||
let action = configured.action.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 trigger = match identity.match_mode {
|
||||
BindingMatch::Keysym => BindingTrigger::Keysym(identity.key),
|
||||
@@ -130,7 +141,9 @@ pub(super) fn binding_identity(
|
||||
) -> Result<BindingIdentity, ConfigError> {
|
||||
let (modifiers, key) = parse_chord(&configured.keys)?;
|
||||
if let Some((fingers, direction)) = parse_swipe(&key) {
|
||||
if configured.match_mode == BindingMatch::Physical || modifiers != BindingModifiers::default() {
|
||||
if configured.match_mode == BindingMatch::Physical
|
||||
|| modifiers != BindingModifiers::default()
|
||||
{
|
||||
return Err(ConfigError::Binding(
|
||||
"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 {
|
||||
BindingTrigger::Keysym(key) => (BindingMatch::Keysym, key),
|
||||
BindingTrigger::Physical(key) => (BindingMatch::Physical, key.raw()),
|
||||
BindingTrigger::Swipe { fingers, direction } => (BindingMatch::Swipe, fingers * 4 + direction as u32),
|
||||
BindingTrigger::Swipe { fingers, direction } => {
|
||||
(BindingMatch::Swipe, fingers * 4 + direction as u32)
|
||||
}
|
||||
};
|
||||
|
||||
BindingIdentity {
|
||||
@@ -198,7 +213,9 @@ pub(super) fn parse_chord(chord: &str) -> Result<(BindingModifiers, String), Con
|
||||
};
|
||||
if let Some(slot) = slot {
|
||||
if *slot {
|
||||
return Err(ConfigError::Binding(format!("duplicate modifier in {chord:?}")));
|
||||
return Err(ConfigError::Binding(format!(
|
||||
"duplicate modifier in {chord:?}"
|
||||
)));
|
||||
}
|
||||
*slot = true;
|
||||
} 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))
|
||||
}
|
||||
|
||||
@@ -264,8 +283,10 @@ pub(super) fn parse_action(
|
||||
Ok(())
|
||||
}
|
||||
};
|
||||
let required_argument =
|
||||
|| argument.ok_or_else(|| ConfigError::Binding(format!("action {action:?} requires an argument")));
|
||||
let required_argument = || {
|
||||
argument
|
||||
.ok_or_else(|| ConfigError::Binding(format!("action {action:?} requires an argument")))
|
||||
};
|
||||
|
||||
match action {
|
||||
"binding-mode" => Ok(BindingAction::BindingMode(required_argument()?.to_owned())),
|
||||
@@ -275,7 +296,9 @@ pub(super) fn parse_action(
|
||||
}
|
||||
"workspace-next" | "workspace-previous" => {
|
||||
no_argument()?;
|
||||
Ok(BindingAction::SwitchRelativeWorkspace(action == "workspace-next"))
|
||||
Ok(BindingAction::SwitchRelativeWorkspace(
|
||||
action == "workspace-next",
|
||||
))
|
||||
}
|
||||
"workspace-back-and-forth" => {
|
||||
no_argument()?;
|
||||
@@ -310,9 +333,15 @@ pub(super) fn parse_action(
|
||||
}
|
||||
"focus" => Ok(BindingAction::Focus(parse_direction(required_argument()?)?)),
|
||||
"move" => Ok(BindingAction::Move(parse_direction(required_argument()?)?)),
|
||||
"resize" => Ok(BindingAction::Resize(parse_direction(required_argument()?)?)),
|
||||
"workspace" => Ok(BindingAction::SwitchWorkspace(parse_workspace(required_argument()?)?)),
|
||||
"move-to-workspace" => Ok(BindingAction::MoveToWorkspace(parse_workspace(required_argument()?)?)),
|
||||
"resize" => Ok(BindingAction::Resize(
|
||||
parse_direction(required_argument()?)?,
|
||||
)),
|
||||
"workspace" => Ok(BindingAction::SwitchWorkspace(parse_workspace(
|
||||
required_argument()?,
|
||||
)?)),
|
||||
"move-to-workspace" => Ok(BindingAction::MoveToWorkspace(parse_workspace(
|
||||
required_argument()?,
|
||||
)?)),
|
||||
"toggle-maximized" => {
|
||||
no_argument()?;
|
||||
Ok(BindingAction::ToggleMaximized)
|
||||
@@ -337,7 +366,9 @@ pub(super) fn parse_action(
|
||||
no_argument()?;
|
||||
Ok(BindingAction::ToggleColumnTabbed)
|
||||
}
|
||||
"join-tab" => Ok(BindingAction::JoinTab(parse_direction(required_argument()?)?)),
|
||||
"join-tab" => Ok(BindingAction::JoinTab(parse_direction(
|
||||
required_argument()?
|
||||
)?)),
|
||||
"leave-tab" => {
|
||||
no_argument()?;
|
||||
Ok(BindingAction::LeaveTab)
|
||||
@@ -396,14 +427,18 @@ pub(super) fn parse_direction(argument: &str) -> Result<Direction, ConfigError>
|
||||
"right" => Ok(Direction::Right),
|
||||
"up" => Ok(Direction::Up),
|
||||
"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> {
|
||||
match argument.parse() {
|
||||
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")] {
|
||||
bindings.push(binding("Super+".to_owned() + key, "focus", Some(direction)));
|
||||
bindings.push(binding("Super+Shift+".to_owned() + key, "move", Some(direction)));
|
||||
bindings.push(binding("Super+Ctrl+".to_owned() + key, "resize", Some(direction)));
|
||||
bindings.push(binding(
|
||||
"Super+Shift+".to_owned() + key,
|
||||
"move",
|
||||
Some(direction),
|
||||
));
|
||||
bindings.push(binding(
|
||||
"Super+Ctrl+".to_owned() + key,
|
||||
"resize",
|
||||
Some(direction),
|
||||
));
|
||||
}
|
||||
|
||||
for workspace in 1..=9 {
|
||||
let workspace = workspace.to_string();
|
||||
bindings.push(binding(format!("Super+{workspace}"), "workspace", Some(&workspace)));
|
||||
bindings.push(binding(
|
||||
format!("Super+{workspace}"),
|
||||
"workspace",
|
||||
Some(&workspace),
|
||||
));
|
||||
bindings.push(binding(
|
||||
format!("Super+Shift+{workspace}"),
|
||||
"move-to-workspace",
|
||||
@@ -463,7 +510,11 @@ pub(super) fn default_bindings() -> Vec<BindingConfig> {
|
||||
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 {
|
||||
keys: keys.into(),
|
||||
match_mode: BindingMatch::Keysym,
|
||||
@@ -557,7 +608,10 @@ impl Binding {
|
||||
let symbol = parse_keysym(&key)?;
|
||||
if (modifiers.ctrl
|
||||
&& modifiers.alt
|
||||
&& matches!(symbol, keysyms::KEY_Escape | keysyms::KEY_F1..=keysyms::KEY_F12))
|
||||
&& matches!(
|
||||
symbol,
|
||||
keysyms::KEY_Escape | keysyms::KEY_F1..=keysyms::KEY_F12
|
||||
))
|
||||
|| (keysyms::KEY_XF86Switch_VT_1..=keysyms::KEY_XF86Switch_VT_12).contains(&symbol)
|
||||
{
|
||||
return Err(ConfigError::Binding("Reserved system shortcut".into()));
|
||||
@@ -576,19 +630,34 @@ impl Binding {
|
||||
match (&self.trigger, &other.trigger) {
|
||||
(BindingTrigger::Keysym(left), BindingTrigger::Keysym(right)) => left == right,
|
||||
(BindingTrigger::Keysym(symbol), BindingTrigger::Physical(code))
|
||||
| (BindingTrigger::Physical(code), BindingTrigger::Keysym(symbol)) => (0..map.num_layouts_for_key(*code))
|
||||
.any(|layout| {
|
||||
| (BindingTrigger::Physical(code), BindingTrigger::Keysym(symbol)) => {
|
||||
(0..map.num_layouts_for_key(*code)).any(|layout| {
|
||||
map.key_get_syms_by_level(*code, layout, 0)
|
||||
.iter()
|
||||
.any(|key| key.raw() == *symbol)
|
||||
}),
|
||||
})
|
||||
}
|
||||
(BindingTrigger::Physical(left), BindingTrigger::Physical(right)) => left == right,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn matches(&self, keycode: Keycode, keysyms: &[u32], logo: bool, ctrl: bool, alt: bool, shift: bool) -> bool {
|
||||
self.modifiers == BindingModifiers { logo, ctrl, alt, shift }
|
||||
pub fn matches(
|
||||
&self,
|
||||
keycode: Keycode,
|
||||
keysyms: &[u32],
|
||||
logo: bool,
|
||||
ctrl: bool,
|
||||
alt: bool,
|
||||
shift: bool,
|
||||
) -> bool {
|
||||
self.modifiers
|
||||
== BindingModifiers {
|
||||
logo,
|
||||
ctrl,
|
||||
alt,
|
||||
shift,
|
||||
}
|
||||
&& match self.trigger {
|
||||
BindingTrigger::Keysym(expected) => keysyms.contains(&expected),
|
||||
BindingTrigger::Physical(expected) => expected == keycode,
|
||||
@@ -600,7 +669,11 @@ impl Binding {
|
||||
let description = match &self.action {
|
||||
BindingAction::BindingMode(name) => format!("Switch to binding mode {name}"),
|
||||
BindingAction::None => return None,
|
||||
BindingAction::Spawn(argv) if argv.first().is_some_and(|program| program == "ferese-screenshot") => {
|
||||
BindingAction::Spawn(argv)
|
||||
if argv
|
||||
.first()
|
||||
.is_some_and(|program| program == "ferese-screenshot") =>
|
||||
{
|
||||
if argv.iter().any(|arg| arg == "--full") {
|
||||
"Capture whole screen"
|
||||
} else {
|
||||
@@ -619,7 +692,9 @@ impl Binding {
|
||||
BindingAction::Exit => "Log out (with confirmation)".into(),
|
||||
BindingAction::Focus(direction) => format!("Focus window {direction:?}"),
|
||||
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 {
|
||||
"Previous window in focus history"
|
||||
} else {
|
||||
@@ -629,11 +704,16 @@ impl Binding {
|
||||
BindingAction::Move(direction) => format!("Move window {direction:?}"),
|
||||
BindingAction::Resize(direction) => format!("Resize window {direction:?}"),
|
||||
BindingAction::SwitchWorkspace(index) => format!("Go to workspace {index}"),
|
||||
BindingAction::WorkspaceBackAndForth => "Return to last workspace on this monitor".into(),
|
||||
BindingAction::MoveToWorkspace(index) => format!("Move window to workspace {index}"),
|
||||
BindingAction::SwitchRelativeWorkspace(next) => {
|
||||
if *next { "Next workspace" } else { "Previous workspace" }.into()
|
||||
BindingAction::WorkspaceBackAndForth => {
|
||||
"Return to last workspace on this monitor".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::ToggleMaximized => "Toggle full-width window".into(),
|
||||
BindingAction::ToggleLayout => "Switch workspace layout".into(),
|
||||
@@ -642,7 +722,12 @@ impl Binding {
|
||||
BindingAction::ToggleColumnTabbed => "Toggle stacked / tabbed column".into(),
|
||||
BindingAction::JoinTab(direction) => format!("Join tab group {direction:?}"),
|
||||
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::Expel => "Leave column or tab group".into(),
|
||||
BindingAction::ToggleFloating => "Toggle floating window".into(),
|
||||
@@ -694,7 +779,11 @@ impl Binding {
|
||||
matches!(self.trigger, BindingTrigger::Swipe { fingers, .. } if fingers == count)
|
||||
}
|
||||
|
||||
pub(crate) fn matches_swipe(&self, count: u32, target: crate::gestures::SwipeDirection) -> bool {
|
||||
pub(crate) fn matches_swipe(
|
||||
&self,
|
||||
count: u32,
|
||||
target: crate::gestures::SwipeDirection,
|
||||
) -> bool {
|
||||
matches!(self.trigger, BindingTrigger::Swipe { fingers, direction } if fingers == count && direction == target)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,12 @@ impl BindingSet {
|
||||
alt: bool,
|
||||
shift: bool,
|
||||
) -> Option<&Binding> {
|
||||
let modifiers = BindingModifiers { logo, ctrl, alt, shift };
|
||||
let modifiers = BindingModifiers {
|
||||
logo,
|
||||
ctrl,
|
||||
alt,
|
||||
shift,
|
||||
};
|
||||
let physical = self.keyboard.get(&BindingIdentity {
|
||||
modifiers,
|
||||
match_mode: BindingMatch::Physical,
|
||||
@@ -96,7 +101,10 @@ mod tests {
|
||||
let code = Keycode::new(38);
|
||||
let bindings = vec![
|
||||
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_b), BindingAction::None),
|
||||
];
|
||||
@@ -125,7 +133,10 @@ mod tests {
|
||||
let config = Config::default();
|
||||
let mut bindings = config.bindings(&config.input_settings().unwrap()).unwrap();
|
||||
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::Swipe {
|
||||
@@ -138,7 +149,8 @@ mod tests {
|
||||
let index = BindingSet::from(bindings);
|
||||
|
||||
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 symbols in [
|
||||
@@ -164,7 +176,10 @@ mod tests {
|
||||
#[test]
|
||||
fn replacing_bindings_discards_old_index_entries() {
|
||||
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!(
|
||||
bindings.action_for_key(code, &[], false, false, false, false),
|
||||
Some(&BindingAction::Close)
|
||||
@@ -173,7 +188,10 @@ mod tests {
|
||||
BindingTrigger::Keysym(keysyms::KEY_b),
|
||||
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!(
|
||||
bindings.action_for_key(code, &[keysyms::KEY_b], false, false, false, false),
|
||||
Some(&BindingAction::None)
|
||||
@@ -182,9 +200,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
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 (BindingAction::Spawn(original), BindingAction::Spawn(cloned)) = (&action, cloned) else {
|
||||
let (BindingAction::Spawn(original), BindingAction::Spawn(cloned)) = (&action, cloned)
|
||||
else {
|
||||
unreachable!();
|
||||
};
|
||||
assert!(Arc::ptr_eq(original, &cloned));
|
||||
|
||||
@@ -75,16 +75,24 @@ impl Config {
|
||||
if configured.disabled
|
||||
|| identity.match_mode == BindingMatch::Swipe
|
||||
|| !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
|
||||
&& (0..keymap.num_layouts_for_key(Keycode::new(identity.key))).any(|layout| {
|
||||
(0..keymap.num_levels_for_key(Keycode::new(identity.key), layout)).any(|level| {
|
||||
&& (0..keymap.num_layouts_for_key(Keycode::new(identity.key))).any(
|
||||
|layout| {
|
||||
(0..keymap.num_levels_for_key(Keycode::new(identity.key), layout))
|
||||
.any(|level| {
|
||||
keymap
|
||||
.key_get_syms_by_level(Keycode::new(identity.key), layout, level)
|
||||
.key_get_syms_by_level(
|
||||
Keycode::new(identity.key),
|
||||
layout,
|
||||
level,
|
||||
)
|
||||
.iter()
|
||||
.any(|symbol| symbol.raw() == keysyms::KEY_Escape)
|
||||
})
|
||||
}))
|
||||
},
|
||||
))
|
||||
{
|
||||
return Err(ConfigError::Binding(format!(
|
||||
"invalid, duplicate or reserved mode binding {:?}",
|
||||
@@ -95,12 +103,17 @@ impl Config {
|
||||
}
|
||||
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
|
||||
&& name != "default"
|
||||
&& !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)
|
||||
@@ -131,7 +144,14 @@ mod tests {
|
||||
assert!(modes.set("resize"));
|
||||
let selected = modes.bindings(&default);
|
||||
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))
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -139,11 +159,25 @@ mod tests {
|
||||
Some(&BindingAction::Resize(Direction::Up))
|
||||
);
|
||||
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
|
||||
);
|
||||
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
|
||||
);
|
||||
assert!(!modes.set("missing"));
|
||||
|
||||
@@ -57,11 +57,15 @@ impl Config {
|
||||
}
|
||||
|
||||
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> {
|
||||
self.animations.viewport_config().map_err(ConfigError::Binding)
|
||||
self.animations
|
||||
.viewport_config()
|
||||
.map_err(ConfigError::Binding)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,7 +160,8 @@ mod tests {
|
||||
#[test]
|
||||
fn compositor_speed_uses_shared_motion_bounds() {
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,11 @@ impl Config {
|
||||
}
|
||||
|
||||
pub fn scrolling_focus_strategy(&self) -> ViewportFocusStrategy {
|
||||
match self.scrolling.focus_strategy.unwrap_or(FocusStrategyValue::Minimal) {
|
||||
match self
|
||||
.scrolling
|
||||
.focus_strategy
|
||||
.unwrap_or(FocusStrategyValue::Minimal)
|
||||
{
|
||||
FocusStrategyValue::Minimal => ViewportFocusStrategy::Minimal,
|
||||
FocusStrategyValue::CenterOnFocus => ViewportFocusStrategy::Center,
|
||||
FocusStrategyValue::Paged => ViewportFocusStrategy::Paged,
|
||||
@@ -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 {
|
||||
Ok(value)
|
||||
} 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 {
|
||||
ColumnWidthValue::Proportion(value) if value.is_finite() && *value > 0.0 => Ok(ColumnWidth::Proportion(*value)),
|
||||
ColumnWidthValue::Named(value) if value.eq_ignore_ascii_case("full") => Ok(ColumnWidth::Full),
|
||||
ColumnWidthValue::Proportion(value) if value.is_finite() && *value > 0.0 => {
|
||||
Ok(ColumnWidth::Proportion(*value))
|
||||
}
|
||||
ColumnWidthValue::Named(value) if value.eq_ignore_ascii_case("full") => {
|
||||
Ok(ColumnWidth::Full)
|
||||
}
|
||||
value => Err(ConfigError::ColumnWidth {
|
||||
field,
|
||||
value: match value {
|
||||
|
||||
@@ -20,7 +20,10 @@ pub(crate) fn read_source(path: &Path) -> Result<String, String> {
|
||||
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())?;
|
||||
prepare_document(&document, directory)
|
||||
}
|
||||
@@ -68,9 +71,11 @@ pub(crate) fn prepare_document(
|
||||
) -> Result<(Config, RuntimeConfig, Candidate), String> {
|
||||
let candidate = crate::theme::resolve_candidate(document, directory)?;
|
||||
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())?;
|
||||
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))
|
||||
}
|
||||
|
||||
@@ -119,7 +124,10 @@ mod tests {
|
||||
let valid = startup(Some(&path)).unwrap();
|
||||
assert!(valid.error.is_none());
|
||||
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!(
|
||||
Document::parse(valid.source.as_deref().unwrap())
|
||||
.unwrap()
|
||||
|
||||
@@ -9,13 +9,19 @@ impl Config {
|
||||
.map(|profile| {
|
||||
let name = profile.name.trim();
|
||||
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()) {
|
||||
return Err(ConfigError::OutputProfile(format!("duplicate profile name {name:?}")));
|
||||
return Err(ConfigError::OutputProfile(format!(
|
||||
"duplicate profile name {name:?}"
|
||||
)));
|
||||
}
|
||||
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();
|
||||
@@ -70,13 +76,16 @@ impl Config {
|
||||
)));
|
||||
}
|
||||
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!(
|
||||
"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!(
|
||||
"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
|
||||
.trim()
|
||||
.split_once('@')
|
||||
.map_or((value.trim(), None), |(size, refresh)| (size, Some(refresh)));
|
||||
.map_or((value.trim(), None), |(size, refresh)| {
|
||||
(size, Some(refresh))
|
||||
});
|
||||
let (width, height) = size
|
||||
.split_once('x')
|
||||
.ok_or_else(|| format!("mode {value:?} must use WIDTHxHEIGHT or WIDTHxHEIGHT@REFRESH"))?;
|
||||
|
||||
@@ -74,8 +74,12 @@ impl Config {
|
||||
Err(ConfigError::LayoutValue { field, value })
|
||||
}
|
||||
};
|
||||
let color =
|
||||
|value: &Option<String>, field| value.as_ref().map(|s| parse_color(s, field).map(|c| c.0)).transpose();
|
||||
let color = |value: &Option<String>, field| {
|
||||
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 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")?;
|
||||
@@ -85,11 +89,21 @@ impl Config {
|
||||
font_size,
|
||||
outline_width: range(c.outline_width, 0.0, 8.0, "tab_indicator.outline_width")?,
|
||||
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")?,
|
||||
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")?,
|
||||
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")?,
|
||||
flash_duration: std::time::Duration::from_millis(range(
|
||||
c.flash_duration_ms as f64,
|
||||
@@ -111,9 +125,18 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
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!(
|
||||
(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.)
|
||||
);
|
||||
assert_eq!(c.flash_duration.as_millis(), 1000);
|
||||
@@ -123,7 +146,10 @@ mod tests {
|
||||
)
|
||||
.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.]));
|
||||
for source in [
|
||||
"width 0",
|
||||
@@ -133,7 +159,9 @@ mod tests {
|
||||
"flash-duration-ms 60001",
|
||||
"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::parse_source("scrolling { tab-indicator { typo 1; }; }").is_err());
|
||||
|
||||
+79
-26
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -64,7 +68,10 @@ impl NamedCursor {
|
||||
let mut offset = elapsed.as_nanos() % period.as_nanos();
|
||||
for (index, frame) in self.frames.iter().enumerate() {
|
||||
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();
|
||||
}
|
||||
@@ -97,7 +104,9 @@ impl Loader {
|
||||
let mut theme = None;
|
||||
move |(icon, scale)| {
|
||||
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)
|
||||
}
|
||||
@@ -135,7 +144,14 @@ impl Loader {
|
||||
drop(requests);
|
||||
let cursor = load(request);
|
||||
let (icon, scale) = request;
|
||||
if sender.send(Loaded { icon, scale, cursor }).is_err() {
|
||||
if sender
|
||||
.send(Loaded {
|
||||
icon,
|
||||
scale,
|
||||
cursor,
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
break;
|
||||
}
|
||||
signal.wakeup();
|
||||
@@ -218,7 +234,12 @@ impl crate::Ferese {
|
||||
}
|
||||
.unwrap_or(1)
|
||||
.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);
|
||||
cursor.requested_scale = scale;
|
||||
let active = match self.cursor_status {
|
||||
@@ -230,12 +251,9 @@ impl crate::Ferese {
|
||||
&& active.is_some_and(|active| !self.named_cursors.contains_key(&active)));
|
||||
let mut bytes: usize = self.named_cursors.values().map(|cursor| cursor.bytes).sum();
|
||||
while bytes + cursor.bytes > MAX_CACHE_BYTES {
|
||||
let Some(evict) = self
|
||||
.named_cursors
|
||||
.keys()
|
||||
.copied()
|
||||
.find(|key| *key != CursorIcon::Default && *key != icon && Some(*key) != active)
|
||||
else {
|
||||
let Some(evict) = self.named_cursors.keys().copied().find(|key| {
|
||||
*key != CursorIcon::Default && *key != icon && Some(*key) != active
|
||||
}) else {
|
||||
break;
|
||||
};
|
||||
bytes -= self.named_cursors.remove(&evict).unwrap().bytes;
|
||||
@@ -243,7 +261,8 @@ impl crate::Ferese {
|
||||
self.named_cursors.insert(icon, cursor);
|
||||
if affects_current {
|
||||
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)
|
||||
.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
|
||||
.and_then(|icon| {
|
||||
@@ -285,7 +305,8 @@ impl crate::Ferese {
|
||||
.get(&icon)
|
||||
.or_else(|| self.named_cursors.get(&CursorIcon::Default))
|
||||
.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 {
|
||||
self.cursor_animation.frame = frame;
|
||||
self.cursor_redraw_pending = true;
|
||||
@@ -293,13 +314,14 @@ impl crate::Ferese {
|
||||
if let Some(remaining) = remaining
|
||||
&& self.cursor_animation.timer.is_none()
|
||||
{
|
||||
match self
|
||||
.loop_handle
|
||||
.insert_source(Timer::from_deadline(now + remaining), |_, _, state| {
|
||||
match self.loop_handle.insert_source(
|
||||
Timer::from_deadline(now + remaining),
|
||||
|_, _, state| {
|
||||
state.cursor_animation.timer = None;
|
||||
state.cursor_redraw_pending = true;
|
||||
TimeoutAction::Drop
|
||||
}) {
|
||||
},
|
||||
) {
|
||||
Ok(token) => self.cursor_animation.timer = Some(token),
|
||||
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;
|
||||
}
|
||||
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)
|
||||
})
|
||||
}
|
||||
@@ -348,7 +372,10 @@ fn parse_cursor(bytes: &[u8], requested: u32, scale: i32) -> Option<NamedCursor>
|
||||
}
|
||||
let header = word(bytes, 4)? 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;
|
||||
}
|
||||
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 xhot = word(bytes, pos + 24)?;
|
||||
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;
|
||||
}
|
||||
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 end = start.checked_add(pixels.checked_mul(4)?)?;
|
||||
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
|
||||
.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))?;
|
||||
let mut frames = Vec::new();
|
||||
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 w = (f64::from(width) * 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;
|
||||
}
|
||||
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 {
|
||||
return None;
|
||||
}
|
||||
let resized;
|
||||
let pixels = if w != width || h != height {
|
||||
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()
|
||||
} else {
|
||||
rgba
|
||||
};
|
||||
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: (
|
||||
(f64::from(xhot) * ratio).round() as i32,
|
||||
(f64::from(yhot) * ratio).round() as i32,
|
||||
|
||||
@@ -7,15 +7,25 @@ use smithay::utils::SERIAL_COUNTER;
|
||||
|
||||
fn file(frames: &[(u32, u32, u32, u32)]) -> Vec<u8> {
|
||||
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;
|
||||
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;
|
||||
}
|
||||
for &(size, width, hotspot, delay) in frames {
|
||||
bytes.extend(
|
||||
[36, 0xfffd0002, size, 1, width, width, hotspot, hotspot, delay]
|
||||
[
|
||||
36, 0xfffd0002, size, 1, width, width, hotspot, hotspot, delay,
|
||||
]
|
||||
.into_iter()
|
||||
.flat_map(u32::to_le_bytes),
|
||||
);
|
||||
@@ -49,12 +59,20 @@ fn unavailable_hidpi_size_preserves_logical_size_and_hotspot() {
|
||||
assert_eq!(frame.buffer_scale, 2);
|
||||
assert_eq!(frame.size.w, 64);
|
||||
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]
|
||||
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);
|
||||
for (time, index, remaining) in [
|
||||
(0, 0, 30),
|
||||
@@ -93,7 +111,11 @@ fn rejects_oversized_and_malformed_assets_before_buffer_allocation() {
|
||||
bytes.truncate(16);
|
||||
bytes[12..16].copy_from_slice(&3u32.to_le_bytes());
|
||||
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);
|
||||
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(),
|
||||
"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();
|
||||
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]
|
||||
@@ -188,7 +216,9 @@ fn cursor_animation_advances_and_cancels_on_hide_and_shape_change() {
|
||||
assert!(state.cursor_animation.timer.is_some());
|
||||
// Invoke the same deadline callback by dispatching the real timer.
|
||||
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));
|
||||
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.update_named_cursor(now + Duration::from_millis(32));
|
||||
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.update_named_cursor(now + Duration::from_millis(33));
|
||||
assert!(state.cursor_animation.timer.is_none());
|
||||
@@ -208,9 +240,18 @@ fn cursor_animation_advances_and_cancels_on_hide_and_shape_change() {
|
||||
|
||||
#[test]
|
||||
fn supplies_legacy_fallback_names_for_common_cursors() {
|
||||
assert_eq!(cursor_names(CursorIcon::Default), ["default", "left_ptr", "arrow"]);
|
||||
assert_eq!(cursor_names(CursorIcon::Pointer), ["pointer", "hand2", "left_ptr"]);
|
||||
assert_eq!(cursor_names(CursorIcon::Text), ["text", "xterm", "left_ptr"]);
|
||||
assert_eq!(
|
||||
cursor_names(CursorIcon::Default),
|
||||
["default", "left_ptr", "arrow"]
|
||||
);
|
||||
assert_eq!(
|
||||
cursor_names(CursorIcon::Pointer),
|
||||
["pointer", "hand2", "left_ptr"]
|
||||
);
|
||||
assert_eq!(
|
||||
cursor_names(CursorIcon::Text),
|
||||
["text", "xterm", "left_ptr"]
|
||||
);
|
||||
assert_eq!(
|
||||
cursor_names(CursorIcon::Crosshair),
|
||||
["crosshair", "default", "left_ptr"]
|
||||
|
||||
+33
-11
@@ -98,15 +98,16 @@ impl Runner {
|
||||
}
|
||||
if let Some(deadline) = deadline {
|
||||
let weak = Rc::downgrade(owner);
|
||||
match state
|
||||
.loop_handle
|
||||
.insert_source(Timer::from_deadline(deadline), move |_, _, state| {
|
||||
match state.loop_handle.insert_source(
|
||||
Timer::from_deadline(deadline),
|
||||
move |_, _, state| {
|
||||
if let Some(owner) = weak.upgrade() {
|
||||
owner.borrow_mut().2 = None;
|
||||
Self::refresh(&owner, state);
|
||||
}
|
||||
TimeoutAction::Drop
|
||||
}) {
|
||||
},
|
||||
) {
|
||||
Ok(token) => runner.2 = Some((token, deadline)),
|
||||
Err(error) => tracing::warn!(%error, "could not arm daemon restart deadline"),
|
||||
}
|
||||
@@ -119,7 +120,11 @@ impl Runner {
|
||||
.iter()
|
||||
.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;
|
||||
}
|
||||
if let Some(index) = previous
|
||||
@@ -187,7 +192,11 @@ impl Runner {
|
||||
}
|
||||
}
|
||||
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);
|
||||
if service.child.is_none() && !service.config.restart {
|
||||
service.finished = true;
|
||||
@@ -225,14 +234,19 @@ mod tests {
|
||||
let mut events = smithay::reexports::calloop::EventLoop::<usize>::try_new().unwrap();
|
||||
events
|
||||
.handle()
|
||||
.insert_source(Generic::new(fd, Interest::READ, Mode::Level), move |_, _, count| {
|
||||
.insert_source(
|
||||
Generic::new(fd, Interest::READ, Mode::Level),
|
||||
move |_, _, count| {
|
||||
assert_eq!(child.wait().unwrap().code(), Some(7));
|
||||
*count += 1;
|
||||
Ok(PostAction::Remove)
|
||||
})
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
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);
|
||||
events.dispatch(Some(Duration::ZERO), &mut count).unwrap();
|
||||
assert_eq!(count, 1);
|
||||
@@ -247,7 +261,12 @@ mod tests {
|
||||
nested: false,
|
||||
};
|
||||
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();
|
||||
config.restart = true;
|
||||
runner.reconcile(&[config.clone()], SessionPolicy::Desktop);
|
||||
@@ -260,7 +279,10 @@ mod tests {
|
||||
assert!(runner.0.is_empty());
|
||||
let deadline = Instant::now() + Duration::from_secs(2);
|
||||
while unsafe { libc::kill(pid as i32, 0) } == 0 {
|
||||
assert!(Instant::now() < deadline, "removed service was not stopped/reaped");
|
||||
assert!(
|
||||
Instant::now() < deadline,
|
||||
"removed service was not stopped/reaped"
|
||||
);
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,9 @@ impl DisplayPresentation {
|
||||
|
||||
#[cfg(test)]
|
||||
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> {
|
||||
@@ -143,7 +145,8 @@ mod tests {
|
||||
states.states.get_mut(&id).unwrap().visible_area = 0;
|
||||
assert!(visible(&states).is_empty());
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,11 @@ impl FeedbackManager {
|
||||
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();
|
||||
self.surfaces.insert(
|
||||
surface.clone(),
|
||||
@@ -35,7 +39,9 @@ impl FeedbackManager {
|
||||
}
|
||||
|
||||
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>) {
|
||||
@@ -57,7 +63,11 @@ impl FeedbackManager {
|
||||
|
||||
pub fn remove(&mut self, global: DmabufGlobal) {
|
||||
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
|
||||
// capabilities, withdraw scanout hints and retire the global. Bound
|
||||
// resources cannot import through the retired global or another GPU.
|
||||
|
||||
@@ -108,8 +108,13 @@ fn subscriptions_update_deduplicate_and_retire_device_preferences() {
|
||||
if *object != 7 || *opcode != 0 {
|
||||
return None;
|
||||
}
|
||||
let bytes = args[2..].iter().flat_map(|word| word.to_ne_bytes()).collect::<Vec<_>>();
|
||||
bytes.starts_with(b"zwp_linux_dmabuf_v1\0").then_some(args[0])
|
||||
let bytes = args[2..]
|
||||
.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");
|
||||
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!(done(&initial, 9), 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[1], 10, "main device remains the renderer");
|
||||
|
||||
@@ -141,11 +149,20 @@ fn subscriptions_update_deduplicate_and_retire_device_preferences() {
|
||||
let changed = messages(&mut wire);
|
||||
assert_eq!(flags(&changed, 9), [TrancheFlags::Scanout.bits(), 0]);
|
||||
assert_eq!(done(&changed, 9), 1);
|
||||
let target = changed.iter().find(|(id, opcode, _)| *id == 9 && *opcode == 4).unwrap();
|
||||
assert_eq!(target.2[1], 20, "scanout tranche targets the primary DRM device");
|
||||
let target = changed
|
||||
.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.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);
|
||||
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
|
||||
dispatch(&mut events, &mut state);
|
||||
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]);
|
||||
state.dmabuf_feedback.refresh(&selected);
|
||||
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.display_handle.flush_clients().unwrap();
|
||||
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);
|
||||
dispatch(&mut events, &mut state);
|
||||
state.dmabuf_feedback.refresh(&HashMap::new());
|
||||
|
||||
@@ -37,7 +37,12 @@ impl<K, C, T> Default for 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> {
|
||||
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);
|
||||
}
|
||||
@@ -89,7 +94,8 @@ pub(crate) struct ImportManager {
|
||||
|
||||
impl Drop for ImportManager {
|
||||
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) {
|
||||
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 {
|
||||
notifier.failed();
|
||||
return;
|
||||
@@ -114,16 +127,23 @@ impl ImportManager {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Err(request) = self
|
||||
.queue
|
||||
if let Err(request) =
|
||||
self.queue
|
||||
.enqueue(global, client.id(), Request { buffer, notifier }, now)
|
||||
{
|
||||
request.notifier.failed();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn process<R: ImportDma>(&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<R: ImportDma>(
|
||||
&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(
|
||||
@@ -171,16 +191,17 @@ impl Ferese {
|
||||
let mut backend = backend.borrow_mut();
|
||||
// Nested Ferese publishes exactly one DMA-BUF global.
|
||||
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 self.dmabuf_imports.timer.is_none() {
|
||||
match self
|
||||
.loop_handle
|
||||
.insert_source(Timer::from_deadline(deadline), |_, _, state| {
|
||||
match self.loop_handle.insert_source(
|
||||
Timer::from_deadline(deadline),
|
||||
|_, _, state| {
|
||||
state.dmabuf_imports.expire(Instant::now());
|
||||
if let Some(deadline) = state.dmabuf_imports.queue.deadline() {
|
||||
TimeoutAction::ToInstant(deadline)
|
||||
@@ -188,7 +209,8 @@ impl Ferese {
|
||||
state.dmabuf_imports.timer = None;
|
||||
TimeoutAction::Drop
|
||||
}
|
||||
}) {
|
||||
},
|
||||
) {
|
||||
Ok(token) => self.dmabuf_imports.timer = Some(token),
|
||||
Err(error) => {
|
||||
tracing::warn!(%error, "could not schedule DMA-BUF import deadline");
|
||||
@@ -234,7 +256,9 @@ mod tests {
|
||||
let now = Instant::now();
|
||||
let mut queue = Queue::default();
|
||||
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));
|
||||
@@ -275,13 +299,18 @@ mod tests {
|
||||
let fd_removed = removed.as_raw_fd();
|
||||
let fd_expired = expired.as_raw_fd();
|
||||
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;
|
||||
queue.process(&1, |_| failures += 1);
|
||||
// SAFETY: F_GETFD observes descriptor validity without modifying it.
|
||||
assert_eq!(unsafe { libc::fcntl(fd_removed, 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(
|
||||
|entry| entry.deadline <= now + MAX_WAIT + Duration::from_secs(1),
|
||||
|_| failures += 1,
|
||||
|
||||
+45
-14
@@ -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::material::v1::server::ferese_material_manager_v1::FereseMaterialManagerV1;
|
||||
use ferese_protocols::material::v1::server::ferese_surface_material_v1::FereseSurfaceMaterialV1;
|
||||
use ferese_protocols::material::v1::server::{ferese_material_manager_v1, ferese_surface_material_v1};
|
||||
use ferese_protocols::material::v1::server::{
|
||||
ferese_material_manager_v1, ferese_surface_material_v1,
|
||||
};
|
||||
use smithay::reexports::wayland_server::backend::ClientId;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::reexports::wayland_server::{
|
||||
@@ -37,10 +39,16 @@ pub(crate) struct ResolvedMaterial {
|
||||
pub shadow: [f64; 3],
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_material(role: SemanticRole, style: MaterialStyle, opacity: f32) -> ResolvedMaterial {
|
||||
pub(crate) fn resolve_material(
|
||||
role: SemanticRole,
|
||||
style: MaterialStyle,
|
||||
opacity: f32,
|
||||
) -> ResolvedMaterial {
|
||||
let shadow = match role {
|
||||
SemanticRole::Panel => [1.0, 5.0, 0.04],
|
||||
SemanticRole::PanelElevated | SemanticRole::Popover | SemanticRole::Menu => [2.0, 8.0, 0.07],
|
||||
SemanticRole::PanelElevated | SemanticRole::Popover | SemanticRole::Menu => {
|
||||
[2.0, 8.0, 0.07]
|
||||
}
|
||||
SemanticRole::Hud => [1.0, 4.0, 0.04],
|
||||
SemanticRole::Notification | SemanticRole::Modal => [3.0, 10.0, 0.09],
|
||||
};
|
||||
@@ -48,7 +56,11 @@ pub(crate) fn resolve_material(role: SemanticRole, style: MaterialStyle, opacity
|
||||
MaterialStyle::Solid => 1.0,
|
||||
MaterialStyle::Translucent => opacity.clamp(0.0, 1.0),
|
||||
};
|
||||
ResolvedMaterial { style, opacity, shadow }
|
||||
ResolvedMaterial {
|
||||
style,
|
||||
opacity,
|
||||
shadow,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
@@ -167,7 +179,9 @@ impl Dispatch<FereseMaterialManagerV1, ()> for Ferese {
|
||||
match request {
|
||||
ferese_material_manager_v1::Request::GetSurfaceMaterial { id, surface } => {
|
||||
let attached = with_states(&surface, |states| {
|
||||
states.data_map.insert_if_missing_threadsafe(SurfaceEffectsState::new);
|
||||
states
|
||||
.data_map
|
||||
.insert_if_missing_threadsafe(SurfaceEffectsState::new);
|
||||
let effects = states.data_map.get::<SurfaceEffectsState>().unwrap();
|
||||
effects.attached.swap(true, Ordering::AcqRel)
|
||||
});
|
||||
@@ -288,7 +302,9 @@ pub(crate) fn surface_region_opacities(surface: &WlSurface) -> 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;
|
||||
}
|
||||
bytes
|
||||
@@ -303,7 +319,9 @@ fn decode_region_opacities(bytes: &[u8]) -> Option<Vec<u32>> {
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
bytes
|
||||
@@ -311,7 +329,8 @@ fn decode_regions(bytes: &[u8]) -> Option<Vec<[f64; 5]>> {
|
||||
.0
|
||||
.iter()
|
||||
.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[3] > 0.0
|
||||
&& values[4] >= 0.0
|
||||
@@ -363,7 +382,9 @@ impl Dispatch<FereseEffectsManagerV1, ()> for Ferese {
|
||||
match request {
|
||||
ferese_effects_manager_v1::Request::GetSurfaceEffects { id, surface } => {
|
||||
let already_attached = with_states(&surface, |states| {
|
||||
states.data_map.insert_if_missing_threadsafe(SurfaceEffectsState::new);
|
||||
states
|
||||
.data_map
|
||||
.insert_if_missing_threadsafe(SurfaceEffectsState::new);
|
||||
states
|
||||
.data_map
|
||||
.get::<SurfaceEffectsState>()
|
||||
@@ -416,8 +437,10 @@ impl Dispatch<FereseSurfaceEffectsV1, SurfaceEffectsUserData> for Ferese {
|
||||
fusion,
|
||||
opacity,
|
||||
} => {
|
||||
let (Some(regions), Some(panel)) = (decode_regions(®ions), decode_panel_shapes(&shapes, fusion))
|
||||
else {
|
||||
let (Some(regions), Some(panel)) = (
|
||||
decode_regions(®ions),
|
||||
decode_panel_shapes(&shapes, fusion),
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
if panel.shapes.is_empty() != regions.is_empty() {
|
||||
@@ -600,13 +623,18 @@ pub(crate) fn surface_opacity(surface: &WlSurface) -> f32 {
|
||||
states
|
||||
.data_map
|
||||
.get::<SurfaceEffectsState>()
|
||||
.map_or(1.0, |effects| *effects.opacity.lock().unwrap() as f32 / 1000.0)
|
||||
.map_or(1.0, |effects| {
|
||||
*effects.opacity.lock().unwrap() as f32 / 1000.0
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn begin_surface_dismiss(surface: &WlSurface) -> bool {
|
||||
with_states(surface, |states| {
|
||||
states.data_map.get::<SurfaceEffectsState>().is_some_and(|effects| {
|
||||
states
|
||||
.data_map
|
||||
.get::<SurfaceEffectsState>()
|
||||
.is_some_and(|effects| {
|
||||
effects.presentation_supported.load(Ordering::Acquire)
|
||||
&& effects.attached.load(Ordering::Acquire)
|
||||
&& !effects.dismissing.swap(true, Ordering::AcqRel)
|
||||
@@ -733,7 +761,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
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(&[]), Some(vec![]));
|
||||
assert!(decode_region_opacities(&bytes[..11]).is_none());
|
||||
|
||||
+64
-20
@@ -19,8 +19,12 @@ pub fn intersection(a: Rect, b: Rect) -> Option<Rect> {
|
||||
|
||||
pub fn clamp_to_work(mut rect: Rect, work: Rect) -> Rect {
|
||||
// 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.y = rect.y.clamp(work.y, work.y + (work.height - rect.height).max(0.0));
|
||||
rect.x = rect
|
||||
.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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
Rect::new(
|
||||
if left { rect.x + rect.width - size.0 } else { rect.x },
|
||||
if top { rect.y + rect.height - size.1 } else { rect.y },
|
||||
if left {
|
||||
rect.x + rect.width - size.0
|
||||
} else {
|
||||
rect.x
|
||||
},
|
||||
if top {
|
||||
rect.y + rect.height - size.1
|
||||
} else {
|
||||
rect.y
|
||||
},
|
||||
size.0,
|
||||
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;
|
||||
if !width.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))
|
||||
.map(|r| r.width * r.height)
|
||||
.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);
|
||||
if best
|
||||
.as_ref()
|
||||
.is_none_or(|(old, _)| cost.0.total_cmp(&old.0).then(cost.1.total_cmp(&old.1)).is_lt())
|
||||
{
|
||||
if best.as_ref().is_none_or(|(old, _)| {
|
||||
cost.0
|
||||
.total_cmp(&old.0)
|
||||
.then(cost.1.total_cmp(&old.1))
|
||||
.is_lt()
|
||||
}) {
|
||||
best = Some((cost, rect));
|
||||
}
|
||||
}
|
||||
@@ -97,7 +117,10 @@ pub struct Cascade {
|
||||
|
||||
impl Cascade {
|
||||
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
|
||||
|| position.1 < work.y
|
||||
|| position.0 + size.0 > work.x + work.width
|
||||
@@ -132,7 +155,10 @@ impl Remembered {
|
||||
|
||||
pub fn restore(&self, outputs: &[(String, Rect)]) -> Option<Rect> {
|
||||
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;
|
||||
}
|
||||
let [x, y, w, h] = self.fractions;
|
||||
@@ -142,11 +168,15 @@ impl Remembered {
|
||||
w * work.width,
|
||||
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;
|
||||
}
|
||||
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)
|
||||
}
|
||||
@@ -169,8 +199,13 @@ struct SaveThread {
|
||||
}
|
||||
|
||||
impl SaveThread {
|
||||
fn start(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()));
|
||||
fn start(
|
||||
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 thread = std::thread::Builder::new()
|
||||
.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 {
|
||||
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([
|
||||
SnapLine {
|
||||
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([
|
||||
SnapLine {
|
||||
target,
|
||||
@@ -420,7 +459,9 @@ mod tests {
|
||||
memory
|
||||
};
|
||||
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("last"));
|
||||
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 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)
|
||||
.min_by(f64::total_cmp)
|
||||
.unwrap();
|
||||
|
||||
@@ -3,7 +3,11 @@ use std::time::Duration;
|
||||
use crate::config::FocusEffectSettings;
|
||||
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) {
|
||||
1.0
|
||||
} else {
|
||||
@@ -16,12 +20,18 @@ impl FocusEffectSettings {
|
||||
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 {
|
||||
return (1.0, 0.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 {
|
||||
self.inactive_dim * (1.0 - progress) * (1.0 - f64::from(overview_opacity))
|
||||
} else {
|
||||
@@ -87,7 +97,11 @@ impl BoundedFade {
|
||||
pub fn advance_visual(&mut self, target: f64, delta: Duration, duration_ms: f64) -> bool {
|
||||
self.advance(
|
||||
target,
|
||||
if self.target != target { Duration::ZERO } else { delta },
|
||||
if self.target != target {
|
||||
Duration::ZERO
|
||||
} else {
|
||||
delta
|
||||
},
|
||||
duration_ms,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -68,7 +68,8 @@ impl FrameScheduler {
|
||||
// A cold GPU is less predictable. Start immediately on idle wakeup.
|
||||
self.interval
|
||||
} 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?;
|
||||
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_target = None;
|
||||
}
|
||||
@@ -148,8 +152,12 @@ impl FrameScheduler {
|
||||
let ratio = (elapsed.as_secs_f64() / 0.5).clamp(0.01, 1.0);
|
||||
let deviation = sample.saturating_sub(self.render_estimate);
|
||||
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_estimate = self.render_estimate.mul_f64(1.0 - ratio) + sample.mul_f64(ratio);
|
||||
self.render_variance = self
|
||||
.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);
|
||||
let late = timestamp.saturating_sub(plan.present_at);
|
||||
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);
|
||||
// CPU submission timings may omit asynchronous GPU work. Increase
|
||||
// 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.
|
||||
self.last_callback = Some(self.last_presented.map_or(now, |last| {
|
||||
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)
|
||||
}));
|
||||
}
|
||||
@@ -308,7 +318,10 @@ mod tests {
|
||||
scheduler.callback_sent(ms(110));
|
||||
scheduler.request(ms(108));
|
||||
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]
|
||||
@@ -319,7 +332,9 @@ mod tests {
|
||||
let unchanged = scheduler.plan(ms(101)).unwrap();
|
||||
scheduler.begin();
|
||||
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));
|
||||
|
||||
scheduler.request(ms(109));
|
||||
@@ -330,13 +345,19 @@ mod tests {
|
||||
// The old timer is dispatched late, after a new frame was queued.
|
||||
// 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(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);
|
||||
|
||||
scheduler.presented(ms(130));
|
||||
assert_eq!(scheduler.callback_deadline(ms(130), ms(130)), Some(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]
|
||||
|
||||
+21
-5
@@ -30,7 +30,11 @@ impl Ramp {
|
||||
|
||||
pub(crate) fn at_temperature(&self, temperature: u16) -> Self {
|
||||
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 {
|
||||
*value = (f64::from(*value) * gain).round() as u16;
|
||||
}
|
||||
@@ -45,14 +49,18 @@ impl Ramp {
|
||||
self.0
|
||||
.iter()
|
||||
.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(),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn read(file: File, size: usize) -> io::Result<Self> {
|
||||
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 {
|
||||
return Err(io::ErrorKind::InvalidData.into());
|
||||
};
|
||||
@@ -73,7 +81,11 @@ impl Ramp {
|
||||
|
||||
pub(crate) fn channels(&self) -> (&[u16], &[u16], &[u16]) {
|
||||
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.;
|
||||
[1., green(temperature) / green(65.), blue(temperature) / blue(65.)]
|
||||
[
|
||||
1.,
|
||||
green(temperature) / green(65.),
|
||||
blue(temperature) / blue(65.),
|
||||
]
|
||||
}
|
||||
@@ -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 {
|
||||
return self.finish(cancelled);
|
||||
};
|
||||
@@ -76,11 +80,14 @@ impl Swipe {
|
||||
};
|
||||
// The preview is bounded. Its visible velocity is zero when pushing
|
||||
// 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.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)
|
||||
}
|
||||
|
||||
@@ -139,7 +146,8 @@ impl Swipe {
|
||||
if cancelled {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use smithay::desktop::Window;
|
||||
use smithay::input::pointer::{
|
||||
AxisFrame, ButtonEvent, GestureHoldBeginEvent, GestureHoldEndEvent, GesturePinchBeginEvent, GesturePinchEndEvent,
|
||||
GesturePinchUpdateEvent, GestureSwipeBeginEvent, GestureSwipeEndEvent, GestureSwipeUpdateEvent, GrabStartData,
|
||||
MotionEvent, PointerGrab, PointerInnerHandle, RelativeMotionEvent,
|
||||
AxisFrame, ButtonEvent, GestureHoldBeginEvent, GestureHoldEndEvent, GesturePinchBeginEvent,
|
||||
GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent, GestureSwipeEndEvent,
|
||||
GestureSwipeUpdateEvent, GrabStartData, MotionEvent, PointerGrab, PointerInnerHandle,
|
||||
RelativeMotionEvent,
|
||||
};
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::utils::{Logical, Point, Size};
|
||||
@@ -61,7 +62,12 @@ impl PointerGrab<Ferese> for MoveSurfaceGrab {
|
||||
handle.relative_motion(data, focus, event);
|
||||
}
|
||||
|
||||
fn button(&mut self, data: &mut Ferese, handle: &mut PointerInnerHandle<'_, Ferese>, event: &ButtonEvent) {
|
||||
fn button(
|
||||
&mut self,
|
||||
data: &mut Ferese,
|
||||
handle: &mut PointerInnerHandle<'_, Ferese>,
|
||||
event: &ButtonEvent,
|
||||
) {
|
||||
handle.button(data, event);
|
||||
if handle.current_pressed().is_empty() {
|
||||
self.finished = true;
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use smithay::desktop::Window;
|
||||
use smithay::input::pointer::{
|
||||
AxisFrame, ButtonEvent, GestureHoldBeginEvent, GestureHoldEndEvent, GesturePinchBeginEvent, GesturePinchEndEvent,
|
||||
GesturePinchUpdateEvent, GestureSwipeBeginEvent, GestureSwipeEndEvent, GestureSwipeUpdateEvent, GrabStartData,
|
||||
MotionEvent, PointerGrab, PointerInnerHandle, RelativeMotionEvent,
|
||||
AxisFrame, ButtonEvent, GestureHoldBeginEvent, GestureHoldEndEvent, GesturePinchBeginEvent,
|
||||
GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent, GestureSwipeEndEvent,
|
||||
GestureSwipeUpdateEvent, GrabStartData, MotionEvent, PointerGrab, PointerInnerHandle,
|
||||
RelativeMotionEvent,
|
||||
};
|
||||
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
@@ -48,7 +49,9 @@ impl ResizeEdge {
|
||||
fn left(self) -> bool {
|
||||
matches!(
|
||||
self.0,
|
||||
xdg_toplevel::ResizeEdge::Left | xdg_toplevel::ResizeEdge::TopLeft | xdg_toplevel::ResizeEdge::BottomLeft
|
||||
xdg_toplevel::ResizeEdge::Left
|
||||
| xdg_toplevel::ResizeEdge::TopLeft
|
||||
| xdg_toplevel::ResizeEdge::BottomLeft
|
||||
)
|
||||
}
|
||||
|
||||
@@ -64,7 +67,9 @@ impl ResizeEdge {
|
||||
fn top(self) -> bool {
|
||||
matches!(
|
||||
self.0,
|
||||
xdg_toplevel::ResizeEdge::Top | xdg_toplevel::ResizeEdge::TopLeft | xdg_toplevel::ResizeEdge::TopRight
|
||||
xdg_toplevel::ResizeEdge::Top
|
||||
| xdg_toplevel::ResizeEdge::TopLeft
|
||||
| xdg_toplevel::ResizeEdge::TopRight
|
||||
)
|
||||
}
|
||||
|
||||
@@ -122,8 +127,12 @@ impl PointerGrab<Ferese> for ResizeSurfaceGrab {
|
||||
let initial = self.initial_rect;
|
||||
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_right = initial.loc.x as f64 + 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_right = initial.loc.x as f64
|
||||
+ 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(
|
||||
raw_left,
|
||||
raw_top,
|
||||
@@ -136,22 +145,36 @@ impl PointerGrab<Ferese> for ResizeSurfaceGrab {
|
||||
self.snap_y.clear();
|
||||
} else {
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
let surface = self.window.toplevel().expect("managed window has a toplevel");
|
||||
let surface = self
|
||||
.window
|
||||
.toplevel()
|
||||
.expect("managed window has a toplevel");
|
||||
let (minimum, maximum) = with_states(surface.wl_surface(), |states| {
|
||||
let mut cached = states.cached_state.get::<SurfaceCachedState>();
|
||||
let state = cached.current();
|
||||
(state.min_size, state.max_size)
|
||||
});
|
||||
let (minimum, maximum) =
|
||||
data.effective_size_constraints(&self.window, (minimum.w, minimum.h), (maximum.w, maximum.h));
|
||||
let (minimum, maximum) = data.effective_size_constraints(
|
||||
&self.window,
|
||||
(minimum.w, minimum.h),
|
||||
(maximum.w, maximum.h),
|
||||
);
|
||||
self.last_size = constrained_size(
|
||||
self.initial_rect.size,
|
||||
delta,
|
||||
@@ -193,13 +216,21 @@ impl PointerGrab<Ferese> for ResizeSurfaceGrab {
|
||||
handle.relative_motion(data, focus, event);
|
||||
}
|
||||
|
||||
fn button(&mut self, data: &mut Ferese, handle: &mut PointerInnerHandle<'_, Ferese>, event: &ButtonEvent) {
|
||||
fn button(
|
||||
&mut self,
|
||||
data: &mut Ferese,
|
||||
handle: &mut PointerInnerHandle<'_, Ferese>,
|
||||
event: &ButtonEvent,
|
||||
) {
|
||||
handle.button(data, event);
|
||||
if handle.current_pressed().is_empty() {
|
||||
self.finished = true;
|
||||
data.remember_floating(&self.window);
|
||||
handle.unset_grab(self, data, event.serial, event.time, true);
|
||||
let surface = self.window.toplevel().expect("managed window has a toplevel");
|
||||
let surface = self
|
||||
.window
|
||||
.toplevel()
|
||||
.expect("managed window has a toplevel");
|
||||
surface.with_pending_state(|state| {
|
||||
state.states.unset(xdg_toplevel::State::Resizing);
|
||||
state.size = Some(self.last_size);
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -297,13 +333,20 @@ impl PointerGrab<Ferese> for ResizeSurfaceGrab {
|
||||
return;
|
||||
}
|
||||
|
||||
let surface = self.window.toplevel().expect("managed window has a toplevel");
|
||||
let surface = self
|
||||
.window
|
||||
.toplevel()
|
||||
.expect("managed window has a toplevel");
|
||||
surface.with_pending_state(|state| {
|
||||
state.states.unset(xdg_toplevel::State::Resizing);
|
||||
state.size = Some(self.initial_rect.size);
|
||||
});
|
||||
surface.send_pending_configure();
|
||||
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;
|
||||
|
||||
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() {
|
||||
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)
|
||||
|
||||
@@ -40,7 +40,9 @@ impl<C: PartialEq> InputHistory<C> {
|
||||
|
||||
fn validate(&self, serial: Serial, client: &C, now: Instant) -> Option<Instant> {
|
||||
self.events.iter().rev().find_map(|(event, owner, time)| {
|
||||
(*event == serial && owner == client && now.saturating_duration_since(*time) <= TOKEN_MAX_AGE)
|
||||
(*event == serial
|
||||
&& owner == client
|
||||
&& now.saturating_duration_since(*time) <= TOKEN_MAX_AGE)
|
||||
.then_some(*time)
|
||||
})
|
||||
}
|
||||
@@ -55,7 +57,8 @@ impl Ferese {
|
||||
}
|
||||
|
||||
if let Some(client) = surface.and_then(|surface| surface.client()) {
|
||||
self.activation_inputs.record(serial, client.id(), Instant::now());
|
||||
self.activation_inputs
|
||||
.record(serial, client.id(), Instant::now());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -86,14 +89,23 @@ impl XdgActivationHandler for Ferese {
|
||||
return false;
|
||||
}
|
||||
|
||||
let Some(time) = self.activation_inputs.validate(*serial, client, Instant::now()) else {
|
||||
let Some(time) = self
|
||||
.activation_inputs
|
||||
.validate(*serial, client, Instant::now())
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
data.user_data.insert_if_missing(|| ApprovedInteraction(time));
|
||||
data.user_data
|
||||
.insert_if_missing(|| ApprovedInteraction(time));
|
||||
true
|
||||
}
|
||||
|
||||
fn request_activation(&mut self, token: XdgActivationToken, data: XdgActivationTokenData, surface: WlSurface) {
|
||||
fn request_activation(
|
||||
&mut self,
|
||||
token: XdgActivationToken,
|
||||
data: XdgActivationTokenData,
|
||||
surface: WlSurface,
|
||||
) {
|
||||
self.xdg_activation_state.remove_token(&token);
|
||||
if self.session_lock.active() || self.input_capture.captures(1) {
|
||||
return;
|
||||
@@ -152,7 +164,9 @@ mod tests {
|
||||
wire.write_all(&bytes).unwrap();
|
||||
};
|
||||
send(&mut wire, 1, 1, &[2]); // wl_display.get_registry
|
||||
events.dispatch(Duration::from_millis(1), &mut state).unwrap();
|
||||
events
|
||||
.dispatch(Duration::from_millis(1), &mut state)
|
||||
.unwrap();
|
||||
state.display_handle.flush_clients().unwrap();
|
||||
wire.set_nonblocking(true).unwrap();
|
||||
let mut globals = Vec::new();
|
||||
@@ -162,10 +176,16 @@ mod tests {
|
||||
let mut seat_name = None;
|
||||
while offset < globals.len() {
|
||||
let header = u32::from_ne_bytes(globals[offset + 4..offset + 8].try_into().unwrap());
|
||||
if u32::from_ne_bytes(globals[offset..offset + 4].try_into().unwrap()) == 2 && header & 0xffff == 0 {
|
||||
let length = u32::from_ne_bytes(globals[offset + 12..offset + 16].try_into().unwrap()) as usize;
|
||||
if u32::from_ne_bytes(globals[offset..offset + 4].try_into().unwrap()) == 2
|
||||
&& header & 0xffff == 0
|
||||
{
|
||||
let length =
|
||||
u32::from_ne_bytes(globals[offset + 12..offset + 16].try_into().unwrap())
|
||||
as usize;
|
||||
if &globals[offset + 16..offset + 16 + length] == b"wl_seat\0" {
|
||||
seat_name = Some(u32::from_ne_bytes(globals[offset + 8..offset + 12].try_into().unwrap()));
|
||||
seat_name = Some(u32::from_ne_bytes(
|
||||
globals[offset + 8..offset + 12].try_into().unwrap(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,7 +200,9 @@ mod tests {
|
||||
);
|
||||
args.extend([7, 3]);
|
||||
send(&mut wire, 2, 0, &args);
|
||||
events.dispatch(Duration::from_millis(1), &mut state).unwrap();
|
||||
events
|
||||
.dispatch(Duration::from_millis(1), &mut state)
|
||||
.unwrap();
|
||||
let seat: WlSeat = state
|
||||
.seat
|
||||
.client_seats(&launcher)
|
||||
@@ -208,7 +230,9 @@ mod tests {
|
||||
!state.token_created(token.clone(), data()),
|
||||
"fresh token renewed stale input"
|
||||
);
|
||||
state.activation_inputs.record(serial, launcher.id(), Instant::now());
|
||||
state
|
||||
.activation_inputs
|
||||
.record(serial, launcher.id(), Instant::now());
|
||||
let approved = data();
|
||||
assert!(state.token_created(token.clone(), approved.clone()));
|
||||
assert!(approved.user_data.get::<ApprovedInteraction>().is_some());
|
||||
@@ -227,7 +251,15 @@ mod tests {
|
||||
// The proof is attached to the token, not the recipient identity. This
|
||||
// is what lets a validated launcher hand it to a different application.
|
||||
assert_ne!(approved.client_id, Some(recipient.id()));
|
||||
assert!(approved.user_data.get::<ApprovedInteraction>().unwrap().0.elapsed() < TOKEN_MAX_AGE);
|
||||
assert!(
|
||||
approved
|
||||
.user_data
|
||||
.get::<ApprovedInteraction>()
|
||||
.unwrap()
|
||||
.0
|
||||
.elapsed()
|
||||
< TOKEN_MAX_AGE
|
||||
);
|
||||
|
||||
// A valid handoff must not focus a recipient before its first buffer.
|
||||
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_wm_base::XdgWmBase;
|
||||
@@ -241,12 +273,18 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
let shell = recipient
|
||||
.create_resource::<XdgWmBase, XdgWmBaseUserData, Ferese>(&state.display_handle, 6, Default::default())
|
||||
.create_resource::<XdgWmBase, XdgWmBaseUserData, Ferese>(
|
||||
&state.display_handle,
|
||||
6,
|
||||
Default::default(),
|
||||
)
|
||||
.unwrap();
|
||||
send(&mut recipient_wire, compositor.id().protocol_id(), 0, &[2]);
|
||||
send(&mut recipient_wire, shell.id().protocol_id(), 2, &[3, 2]);
|
||||
send(&mut recipient_wire, 3, 1, &[4]);
|
||||
events.dispatch(Duration::from_millis(1), &mut state).unwrap();
|
||||
events
|
||||
.dispatch(Duration::from_millis(1), &mut state)
|
||||
.unwrap();
|
||||
let target = state
|
||||
.space
|
||||
.elements()
|
||||
@@ -272,7 +310,11 @@ mod tests {
|
||||
assert_eq!(history.validate(Serial::from(13), &"launcher", now), None);
|
||||
assert_eq!(history.validate(serial, &"other-client", now), None);
|
||||
assert_eq!(
|
||||
history.validate(serial, &"launcher", now + TOKEN_MAX_AGE + Duration::from_millis(1)),
|
||||
history.validate(
|
||||
serial,
|
||||
&"launcher",
|
||||
now + TOKEN_MAX_AGE + Duration::from_millis(1)
|
||||
),
|
||||
None
|
||||
);
|
||||
// An approved token retains the interaction time across client handoff.
|
||||
|
||||
@@ -5,7 +5,8 @@ use smithay::reexports::wayland_server::protocol::wl_buffer;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::wayland::buffer::BufferHandler;
|
||||
use smithay::wayland::compositor::{
|
||||
CompositorClientState, CompositorHandler, CompositorState, get_parent, get_role, is_sync_subsurface,
|
||||
CompositorClientState, CompositorHandler, CompositorState, get_parent, get_role,
|
||||
is_sync_subsurface,
|
||||
};
|
||||
use smithay::wayland::dmabuf::{DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier};
|
||||
use smithay::wayland::shm::{ShmHandler, ShmState};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::Ferese;
|
||||
use smithay::reexports::wayland_server::{
|
||||
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, backend::ClientId, protocol as wl,
|
||||
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, backend::ClientId,
|
||||
protocol as wl,
|
||||
};
|
||||
use smithay::reexports::{
|
||||
wayland_protocols as protocols, wayland_protocols_misc as misc, wayland_protocols_wlr as wlr,
|
||||
|
||||
@@ -5,7 +5,9 @@ use smithay::reexports::wayland_protocols_wlr::gamma_control::v1::server::{
|
||||
zwlr_gamma_control_manager_v1::{self, ZwlrGammaControlManagerV1},
|
||||
zwlr_gamma_control_v1::{self, ZwlrGammaControlV1},
|
||||
};
|
||||
use smithay::reexports::wayland_server::{Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource};
|
||||
use smithay::reexports::wayland_server::{
|
||||
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
|
||||
};
|
||||
|
||||
use crate::{Ferese, backends::direct::gamma, gamma::Ramp};
|
||||
|
||||
|
||||
@@ -7,7 +7,10 @@ use crate::Ferese;
|
||||
|
||||
impl InputMethodHandler for Ferese {
|
||||
fn new_popup(&mut self, surface: PopupSurface) {
|
||||
if let Err(error) = self.popups.track_popup(PopupKind::InputMethod(surface.clone())) {
|
||||
if let Err(error) = self
|
||||
.popups
|
||||
.track_popup(PopupKind::InputMethod(surface.clone()))
|
||||
{
|
||||
tracing::warn!(?error, "failed to track input method popup");
|
||||
}
|
||||
crate::backends::direct::render_surface(self, surface.wl_surface());
|
||||
@@ -34,7 +37,10 @@ impl InputMethodHandler for Ferese {
|
||||
return geometry;
|
||||
}
|
||||
|
||||
let Some(layer) = self.space.layer_for_surface(parent, WindowSurfaceType::TOPLEVEL) else {
|
||||
let Some(layer) = self
|
||||
.space
|
||||
.layer_for_surface(parent, WindowSurfaceType::TOPLEVEL)
|
||||
else {
|
||||
return Rectangle::default();
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use smithay::desktop::{LayerSurface as DesktopLayerSurface, PopupKind, WindowSurfaceType, layer_map_for_output};
|
||||
use smithay::desktop::{
|
||||
LayerSurface as DesktopLayerSurface, PopupKind, WindowSurfaceType, layer_map_for_output,
|
||||
};
|
||||
use smithay::output::Output;
|
||||
use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
@@ -14,7 +16,13 @@ impl WlrLayerShellHandler for Ferese {
|
||||
&mut self.layer_shell_state
|
||||
}
|
||||
|
||||
fn new_layer_surface(&mut self, surface: LayerSurface, output: Option<WlOutput>, _layer: Layer, namespace: String) {
|
||||
fn new_layer_surface(
|
||||
&mut self,
|
||||
surface: LayerSurface,
|
||||
output: Option<WlOutput>,
|
||||
_layer: Layer,
|
||||
namespace: String,
|
||||
) {
|
||||
// A wl_output resource can outlive its registered desktop output.
|
||||
// Explicit requests retain their identity; only unspecified requests
|
||||
// may use the focused/default output policy.
|
||||
@@ -82,7 +90,11 @@ impl WlrLayerShellHandler for Ferese {
|
||||
let outputs = self
|
||||
.space
|
||||
.outputs()
|
||||
.filter(|output| layer_map_for_output(output).layer_geometry(&layer).is_some())
|
||||
.filter(|output| {
|
||||
layer_map_for_output(output)
|
||||
.layer_geometry(&layer)
|
||||
.is_some()
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@@ -134,13 +146,20 @@ impl LayoutState {
|
||||
}
|
||||
|
||||
pub fn handle_commit(state: &mut Ferese, surface: &WlSurface) {
|
||||
let Some(layer) = state.space.layer_for_surface(surface, WindowSurfaceType::TOPLEVEL) else {
|
||||
let Some(layer) = state
|
||||
.space
|
||||
.layer_for_surface(surface, WindowSurfaceType::TOPLEVEL)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let outputs = state
|
||||
.space
|
||||
.outputs()
|
||||
.filter(|output| layer_map_for_output(output).layer_geometry(&layer).is_some())
|
||||
.filter(|output| {
|
||||
layer_map_for_output(output)
|
||||
.layer_geometry(&layer)
|
||||
.is_some()
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@@ -154,9 +173,9 @@ pub fn handle_commit(state: &mut Ferese, surface: &WlSurface) {
|
||||
}
|
||||
let policy = layer.cached_state();
|
||||
let current = LayoutState::current(&layer);
|
||||
let previous = layer
|
||||
.user_data()
|
||||
.get_or_insert_threadsafe(|| std::sync::Mutex::new(None::<(LayoutState, KeyboardInteractivity)>));
|
||||
let previous = layer.user_data().get_or_insert_threadsafe(|| {
|
||||
std::sync::Mutex::new(None::<(LayoutState, KeyboardInteractivity)>)
|
||||
});
|
||||
let (changed, requested_focus) = {
|
||||
let mut previous = previous.lock().unwrap();
|
||||
let changed = previous.as_ref().map(|(layout, _)| layout) != Some(¤t);
|
||||
@@ -191,7 +210,10 @@ pub fn handle_commit(state: &mut Ferese, surface: &WlSurface) {
|
||||
};
|
||||
state.reconcile_layer_keyboard_focus();
|
||||
if opening_interaction
|
||||
&& !state.seat.get_keyboard().is_some_and(|keyboard| keyboard.is_grabbed())
|
||||
&& !state
|
||||
.seat
|
||||
.get_keyboard()
|
||||
.is_some_and(|keyboard| keyboard.is_grabbed())
|
||||
&& !state.session_lock.active()
|
||||
&& !state.input_capture.captures(1)
|
||||
&& state.layer_keyboard_eligible(&layer)
|
||||
@@ -199,7 +221,11 @@ pub fn handle_commit(state: &mut Ferese, surface: &WlSurface) {
|
||||
&& matches!(policy.layer, Layer::Top | Layer::Overlay)
|
||||
&& !layer_has_keyboard_focus(state, &layer)
|
||||
{
|
||||
state.seat.get_keyboard().expect("seat has a keyboard").set_focus(
|
||||
state
|
||||
.seat
|
||||
.get_keyboard()
|
||||
.expect("seat has a keyboard")
|
||||
.set_focus(
|
||||
state,
|
||||
Some(layer.wl_surface().clone()),
|
||||
smithay::utils::SERIAL_COUNTER.next_serial(),
|
||||
@@ -220,9 +246,10 @@ impl Ferese {
|
||||
/// Use the renderer state updated by on_commit_buffer_handler, as for windows.
|
||||
fn layer_mapped_and_visible(&self, layer: &DesktopLayerSurface) -> bool {
|
||||
layer.layer_surface().alive()
|
||||
&& smithay::backend::renderer::utils::with_renderer_surface_state(layer.wl_surface(), |state| {
|
||||
state.buffer().is_some()
|
||||
})
|
||||
&& smithay::backend::renderer::utils::with_renderer_surface_state(
|
||||
layer.wl_surface(),
|
||||
|state| state.buffer().is_some(),
|
||||
)
|
||||
.unwrap_or(false)
|
||||
&& self.space.outputs().any(|output| {
|
||||
self.layer_visible_on_output(output, layer.layer())
|
||||
@@ -239,7 +266,8 @@ impl Ferese {
|
||||
.find_popup(&surface)
|
||||
.and_then(|popup| smithay::desktop::find_popup_root_surface(&popup).ok())
|
||||
.unwrap_or(surface);
|
||||
(smithay::wayland::compositor::get_role(&root) == Some(smithay::wayland::shell::wlr_layer::LAYER_SURFACE_ROLE))
|
||||
(smithay::wayland::compositor::get_role(&root)
|
||||
== Some(smithay::wayland::shell::wlr_layer::LAYER_SURFACE_ROLE))
|
||||
.then_some(root)
|
||||
}
|
||||
|
||||
@@ -255,7 +283,8 @@ impl Ferese {
|
||||
for priority in [Layer::Overlay, Layer::Top] {
|
||||
if let Some(layer) = focused.as_ref().filter(|layer| {
|
||||
layer.layer() == priority
|
||||
&& layer.cached_state().keyboard_interactivity == KeyboardInteractivity::Exclusive
|
||||
&& layer.cached_state().keyboard_interactivity
|
||||
== KeyboardInteractivity::Exclusive
|
||||
&& self.layer_keyboard_eligible(layer)
|
||||
}) {
|
||||
return Some(layer.clone());
|
||||
@@ -313,7 +342,9 @@ impl Ferese {
|
||||
}
|
||||
|
||||
fn layer_has_keyboard_focus(state: &Ferese, layer: &DesktopLayerSurface) -> bool {
|
||||
state.keyboard_focused_layer().is_some_and(|focused| focused == *layer)
|
||||
state
|
||||
.keyboard_focused_layer()
|
||||
.is_some_and(|focused| focused == *layer)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -349,7 +380,10 @@ mod tests {
|
||||
original,
|
||||
LayoutState::from_policy(policy, Rectangle::from_size((800, 33).into()))
|
||||
);
|
||||
assert_ne!(original, LayoutState::from_policy(policy, Rectangle::default()));
|
||||
assert_ne!(
|
||||
original,
|
||||
LayoutState::from_policy(policy, Rectangle::default())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -151,7 +151,13 @@ impl Client {
|
||||
) -> Vec<Vec<u32>> {
|
||||
if output == 0 {
|
||||
// Reserve the output slot with a spare surface for unspecified requests.
|
||||
request(&mut self.wire, self.compositor.id().protocol_id(), 0, &[5], None);
|
||||
request(
|
||||
&mut self.wire,
|
||||
self.compositor.id().protocol_id(),
|
||||
0,
|
||||
&[5],
|
||||
None,
|
||||
);
|
||||
}
|
||||
request(
|
||||
&mut self.wire,
|
||||
@@ -199,8 +205,9 @@ fn assert_closed_without_configure(messages: &[Vec<u32>]) {
|
||||
|
||||
#[test]
|
||||
fn zero_outputs_close_new_layer_roles() {
|
||||
if !crate::startup_tests::private_runtime("handlers::layer_shell::output_tests::zero_outputs_close_new_layer_roles")
|
||||
{
|
||||
if !crate::startup_tests::private_runtime(
|
||||
"handlers::layer_shell::output_tests::zero_outputs_close_new_layer_roles",
|
||||
) {
|
||||
return;
|
||||
}
|
||||
let (mut state, mut events) = fixture();
|
||||
@@ -259,8 +266,9 @@ fn unspecified_output_uses_focused_registered_output() {
|
||||
|
||||
#[test]
|
||||
fn valid_explicit_output_is_preserved() {
|
||||
if !crate::startup_tests::private_runtime("handlers::layer_shell::output_tests::valid_explicit_output_is_preserved")
|
||||
{
|
||||
if !crate::startup_tests::private_runtime(
|
||||
"handlers::layer_shell::output_tests::valid_explicit_output_is_preserved",
|
||||
) {
|
||||
return;
|
||||
}
|
||||
let (mut state, mut events) = fixture();
|
||||
|
||||
@@ -28,7 +28,9 @@ use smithay::wayland::keyboard_shortcuts_inhibit::{
|
||||
KeyboardShortcutsInhibitorSeat,
|
||||
};
|
||||
use smithay::wayland::output::OutputHandler;
|
||||
use smithay::wayland::pointer_constraints::{PointerConstraint, PointerConstraintsHandler, with_pointer_constraint};
|
||||
use smithay::wayland::pointer_constraints::{
|
||||
PointerConstraint, PointerConstraintsHandler, with_pointer_constraint,
|
||||
};
|
||||
use smithay::wayland::selection::SelectionHandler;
|
||||
use smithay::wayland::selection::data_device::{
|
||||
DataDeviceHandler, DataDeviceState, WaylandDndGrabHandler, set_data_device_focus,
|
||||
@@ -54,12 +56,17 @@ impl SeatHandler for Ferese {
|
||||
if let CursorImageStatus::Named(icon) = &image
|
||||
&& !self.named_cursors.contains_key(icon)
|
||||
{
|
||||
self.cursor_loader.request(*icon, self.cursor_animation.buffer_scale);
|
||||
self.cursor_loader
|
||||
.request(*icon, self.cursor_animation.buffer_scale);
|
||||
}
|
||||
self.cursor_animation.reset |= match (&self.cursor_status, &image) {
|
||||
(CursorImageStatus::Named(previous), CursorImageStatus::Named(next)) => previous != next,
|
||||
(CursorImageStatus::Named(previous), CursorImageStatus::Named(next)) => {
|
||||
previous != next
|
||||
}
|
||||
(CursorImageStatus::Hidden, CursorImageStatus::Hidden) => false,
|
||||
(CursorImageStatus::Surface(previous), CursorImageStatus::Surface(next)) => previous != next,
|
||||
(CursorImageStatus::Surface(previous), CursorImageStatus::Surface(next)) => {
|
||||
previous != next
|
||||
}
|
||||
_ => true,
|
||||
};
|
||||
self.cursor_status = image;
|
||||
@@ -74,7 +81,10 @@ impl SeatHandler for Ferese {
|
||||
self.pending_layer_focus = None;
|
||||
// Smithay has already sent enter. last_enter uses a separate mutex;
|
||||
// querying current_focus here would reenter the held keyboard lock.
|
||||
if let Some(serial) = seat.get_keyboard().and_then(|keyboard| keyboard.last_enter()) {
|
||||
if let Some(serial) = seat
|
||||
.get_keyboard()
|
||||
.and_then(|keyboard| keyboard.last_enter())
|
||||
{
|
||||
self.record_activation_input(serial, focused.cloned());
|
||||
}
|
||||
|
||||
@@ -86,7 +96,9 @@ impl SeatHandler for Ferese {
|
||||
{
|
||||
inhibitor.inactivate();
|
||||
}
|
||||
if let Some(inhibitor) = focused.and_then(|surface| seat.keyboard_shortcuts_inhibitor_for_surface(surface)) {
|
||||
if let Some(inhibitor) =
|
||||
focused.and_then(|surface| seat.keyboard_shortcuts_inhibitor_for_surface(surface))
|
||||
{
|
||||
inhibitor.activate();
|
||||
self.active_shortcuts_inhibitor = Some(inhibitor);
|
||||
}
|
||||
@@ -136,7 +148,10 @@ impl KeyboardShortcutsInhibitHandler for Ferese {
|
||||
}
|
||||
|
||||
fn new_inhibitor(&mut self, inhibitor: KeyboardShortcutsInhibitor) {
|
||||
let focused = self.seat.get_keyboard().and_then(|keyboard| keyboard.current_focus());
|
||||
let focused = self
|
||||
.seat
|
||||
.get_keyboard()
|
||||
.and_then(|keyboard| keyboard.current_focus());
|
||||
|
||||
if focused.as_ref() == Some(inhibitor.wl_surface()) {
|
||||
inhibitor.activate();
|
||||
@@ -216,7 +231,9 @@ impl Ferese {
|
||||
let output = output
|
||||
.or_else(|| self.focused_output())
|
||||
.or_else(|| self.space.outputs().next());
|
||||
with_states(surface, |states| set_surface_preferences(surface, states, output));
|
||||
with_states(surface, |states| {
|
||||
set_surface_preferences(surface, states, output)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,9 +243,18 @@ fn set_surface_preferences(
|
||||
output: Option<&smithay::output::Output>,
|
||||
) {
|
||||
let scale = output.map_or(1.0, |output| output.current_scale().fractional_scale());
|
||||
let transform = output.map_or(smithay::utils::Transform::Normal, |output| output.current_transform());
|
||||
smithay::wayland::compositor::send_surface_state(surface, states, scale.ceil() as i32, transform);
|
||||
with_fractional_scale(states, |surface_scale| surface_scale.set_preferred_scale(scale));
|
||||
let transform = output.map_or(smithay::utils::Transform::Normal, |output| {
|
||||
output.current_transform()
|
||||
});
|
||||
smithay::wayland::compositor::send_surface_state(
|
||||
surface,
|
||||
states,
|
||||
scale.ceil() as i32,
|
||||
transform,
|
||||
);
|
||||
with_fractional_scale(states, |surface_scale| {
|
||||
surface_scale.set_preferred_scale(scale)
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn set_surface_tree_output(surface: &WlSurface, output: &smithay::output::Output) {
|
||||
@@ -337,13 +363,17 @@ impl PrimarySelectionHandler for Ferese {
|
||||
}
|
||||
|
||||
impl smithay::wayland::selection::wlr_data_control::DataControlHandler for Ferese {
|
||||
fn data_control_state(&mut self) -> &mut smithay::wayland::selection::wlr_data_control::DataControlState {
|
||||
fn data_control_state(
|
||||
&mut self,
|
||||
) -> &mut smithay::wayland::selection::wlr_data_control::DataControlState {
|
||||
&mut self.wlr_data_control_state
|
||||
}
|
||||
}
|
||||
|
||||
impl smithay::wayland::selection::ext_data_control::DataControlHandler for Ferese {
|
||||
fn data_control_state(&mut self) -> &mut smithay::wayland::selection::ext_data_control::DataControlState {
|
||||
fn data_control_state(
|
||||
&mut self,
|
||||
) -> &mut smithay::wayland::selection::ext_data_control::DataControlState {
|
||||
&mut self.ext_data_control_state
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,9 @@ use smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::{
|
||||
};
|
||||
use smithay::reexports::wayland_server::protocol::wl_buffer::WlBuffer;
|
||||
use smithay::reexports::wayland_server::protocol::wl_shm;
|
||||
use smithay::reexports::wayland_server::{Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource};
|
||||
use smithay::reexports::wayland_server::{
|
||||
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
|
||||
};
|
||||
use smithay::utils::{Buffer, Logical, Rectangle, Size};
|
||||
use smithay::wayland::shm::with_buffer_contents_mut;
|
||||
|
||||
@@ -157,7 +159,8 @@ impl PendingScreencopy {
|
||||
|
||||
pub(crate) fn capture_allowed() -> bool {
|
||||
static ALLOWED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
|
||||
*ALLOWED.get_or_init(|| capture_opted_in(std::env::var_os("FERESE_ENABLE_SCREENCOPY").as_deref()))
|
||||
*ALLOWED
|
||||
.get_or_init(|| capture_opted_in(std::env::var_os("FERESE_ENABLE_SCREENCOPY").as_deref()))
|
||||
}
|
||||
|
||||
fn capture_opted_in(value: Option<&std::ffi::OsStr>) -> bool {
|
||||
@@ -321,7 +324,9 @@ impl Ferese {
|
||||
let captures = std::mem::take(&mut self.pending_screencopies);
|
||||
|
||||
for capture in captures {
|
||||
if &capture.output != output || !cursor_overlay_matches(capture.overlay_cursor, overlay_cursor) {
|
||||
if &capture.output != output
|
||||
|| !cursor_overlay_matches(capture.overlay_cursor, overlay_cursor)
|
||||
{
|
||||
remaining.push(capture);
|
||||
continue;
|
||||
}
|
||||
@@ -332,7 +337,8 @@ impl Ferese {
|
||||
}
|
||||
|
||||
framebuffer_binding_changed = true;
|
||||
if complete_capture(renderer, framebuffer, &capture, self.start_time.elapsed()).is_err() {
|
||||
if complete_capture(renderer, framebuffer, &capture, self.start_time.elapsed()).is_err()
|
||||
{
|
||||
capture.fail();
|
||||
}
|
||||
}
|
||||
@@ -342,15 +348,18 @@ impl Ferese {
|
||||
}
|
||||
|
||||
pub(crate) fn has_pending_screencopy(&self, output: &Output, overlay_cursor: bool) -> bool {
|
||||
self.pending_screencopies
|
||||
.iter()
|
||||
.any(|capture| capture.output == *output && cursor_overlay_matches(capture.overlay_cursor, overlay_cursor))
|
||||
self.pending_screencopies.iter().any(|capture| {
|
||||
capture.output == *output
|
||||
&& cursor_overlay_matches(capture.overlay_cursor, overlay_cursor)
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn fail_screencopies(&mut self, output: &Output, overlay_cursor: bool) {
|
||||
let mut remaining = Vec::new();
|
||||
for capture in std::mem::take(&mut self.pending_screencopies) {
|
||||
if capture.output == *output && cursor_overlay_matches(capture.overlay_cursor, overlay_cursor) {
|
||||
if capture.output == *output
|
||||
&& cursor_overlay_matches(capture.overlay_cursor, overlay_cursor)
|
||||
{
|
||||
capture.sink.fail();
|
||||
} else {
|
||||
remaining.push(capture);
|
||||
@@ -422,7 +431,10 @@ fn create_frame(
|
||||
}
|
||||
}
|
||||
|
||||
fn capture_region(output: &Output, requested: Option<Rectangle<i32, Logical>>) -> Option<Rectangle<i32, Buffer>> {
|
||||
fn capture_region(
|
||||
output: &Output,
|
||||
requested: Option<Rectangle<i32, Logical>>,
|
||||
) -> Option<Rectangle<i32, Buffer>> {
|
||||
let mode = output.current_mode()?;
|
||||
let scale = output.current_scale().fractional_scale();
|
||||
let transform = output.current_transform();
|
||||
@@ -458,7 +470,9 @@ pub(crate) fn capture_region_for_geometry(
|
||||
}
|
||||
let logical_size = output_logical_size(mode_size, scale, transform);
|
||||
let output_region = Rectangle::from_size(logical_size);
|
||||
let logical_region = requested.unwrap_or(output_region).intersection(output_region)?;
|
||||
let logical_region = requested
|
||||
.unwrap_or(output_region)
|
||||
.intersection(output_region)?;
|
||||
|
||||
if logical_region.size.w <= 0 || logical_region.size.h <= 0 {
|
||||
return None;
|
||||
@@ -509,7 +523,9 @@ where
|
||||
tracing::warn!(?error, "failed to map output framebuffer copy");
|
||||
})?;
|
||||
let expected = capture.region.size;
|
||||
let stride = (expected.w as usize).checked_mul(BYTES_PER_PIXEL).ok_or(())?;
|
||||
let stride = (expected.w as usize)
|
||||
.checked_mul(BYTES_PER_PIXEL)
|
||||
.ok_or(())?;
|
||||
let bytes = stride.checked_mul(expected.h as usize).ok_or(())?;
|
||||
if source.len() < bytes {
|
||||
return Err(());
|
||||
@@ -567,7 +583,11 @@ where
|
||||
frame.flags(zwlr_screencopy_frame_v1::Flags::empty());
|
||||
|
||||
let seconds = timestamp.as_secs();
|
||||
frame.ready((seconds >> 32) as u32, seconds as u32, timestamp.subsec_nanos());
|
||||
frame.ready(
|
||||
(seconds >> 32) as u32,
|
||||
seconds as u32,
|
||||
timestamp.subsec_nanos(),
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -596,7 +616,8 @@ mod tests {
|
||||
let mode = Size::<i32, Physical>::from((1_920, 1_080));
|
||||
let requested = Rectangle::new((-10, 20).into(), (110, 50).into());
|
||||
|
||||
let region = capture_region_for_geometry(mode, 2.0, Transform::Normal, Some(requested)).unwrap();
|
||||
let region =
|
||||
capture_region_for_geometry(mode, 2.0, Transform::Normal, Some(requested)).unwrap();
|
||||
|
||||
assert_eq!(region, Rectangle::new((0, 40).into(), (200, 100).into()));
|
||||
}
|
||||
@@ -606,7 +627,9 @@ mod tests {
|
||||
let mode = Size::<i32, Physical>::from((1_920, 1_080));
|
||||
let requested = Rectangle::new((1_000, 600).into(), (100, 100).into());
|
||||
|
||||
assert!(capture_region_for_geometry(mode, 2.0, Transform::Normal, Some(requested),).is_none());
|
||||
assert!(
|
||||
capture_region_for_geometry(mode, 2.0, Transform::Normal, Some(requested),).is_none()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -40,7 +40,9 @@ const MAX_CANVAS_BYTES: usize = 128 * 1024 * 1024;
|
||||
|
||||
impl Canvas {
|
||||
fn new(width: u32, height: u32) -> Option<Self> {
|
||||
let length = (width as usize).checked_mul(height as usize)?.checked_mul(4)?;
|
||||
let length = (width as usize)
|
||||
.checked_mul(height as usize)?
|
||||
.checked_mul(4)?;
|
||||
if width == 0 || height == 0 || length > MAX_CANVAS_BYTES {
|
||||
return None;
|
||||
}
|
||||
@@ -83,7 +85,11 @@ impl Canvas {
|
||||
}
|
||||
}
|
||||
|
||||
fn scale_and_convert(frame: &OutputFrame, target_width: usize, target_height: usize) -> Option<Vec<u8>> {
|
||||
fn scale_and_convert(
|
||||
frame: &OutputFrame,
|
||||
target_width: usize,
|
||||
target_height: usize,
|
||||
) -> Option<Vec<u8>> {
|
||||
let (upright_width, upright_height) = upright_size(frame);
|
||||
if upright_width <= 0 || upright_height <= 0 || target_width == 0 || target_height == 0 {
|
||||
return None;
|
||||
@@ -95,8 +101,13 @@ fn scale_and_convert(frame: &OutputFrame, target_width: usize, target_height: us
|
||||
if upright_width as usize == target_width && upright_height as usize == target_height {
|
||||
for row in 0..target_height {
|
||||
for column in 0..target_width {
|
||||
let (x, y) =
|
||||
upright_source_pixel(column as i32, row as i32, frame.width, frame.height, frame.transform);
|
||||
let (x, y) = upright_source_pixel(
|
||||
column as i32,
|
||||
row as i32,
|
||||
frame.width,
|
||||
frame.height,
|
||||
frame.transform,
|
||||
);
|
||||
let from = (y as usize)
|
||||
.checked_mul(frame.stride)?
|
||||
.checked_add(x as usize * BYTES_PER_PIXEL)?;
|
||||
@@ -147,7 +158,13 @@ fn scale_and_convert(frame: &OutputFrame, target_width: usize, target_height: us
|
||||
Some(out)
|
||||
}
|
||||
|
||||
fn upright_source_pixel(x: i32, y: i32, width: i32, height: i32, transform: Transform) -> (i32, i32) {
|
||||
fn upright_source_pixel(
|
||||
x: i32,
|
||||
y: i32,
|
||||
width: i32,
|
||||
height: i32,
|
||||
transform: Transform,
|
||||
) -> (i32, i32) {
|
||||
match transform {
|
||||
Transform::_90 => (y, height - 1 - x),
|
||||
Transform::_180 => (width - 1 - x, height - 1 - y),
|
||||
@@ -173,7 +190,10 @@ pub(crate) fn compose(frames: &[OutputFrame]) -> Result<Canvas, String> {
|
||||
}
|
||||
|
||||
let (left, top) = logical_origin(frames).ok_or("Screenshot region is empty")?;
|
||||
let scale = frames.iter().map(|frame| frame.scale).fold(0.0_f64, f64::max);
|
||||
let scale = frames
|
||||
.iter()
|
||||
.map(|frame| frame.scale)
|
||||
.fold(0.0_f64, f64::max);
|
||||
if !(scale.is_finite() && scale > 0.0) {
|
||||
return Err("Screenshot outputs have an invalid scale".into());
|
||||
}
|
||||
@@ -200,15 +220,22 @@ pub(crate) fn compose(frames: &[OutputFrame]) -> Result<Canvas, String> {
|
||||
if width <= 0 || height <= 0 {
|
||||
return Err("Screenshot region is empty".into());
|
||||
}
|
||||
let mut canvas = Canvas::new(width as u32, height as u32).ok_or("Screenshot image is too large to encode")?;
|
||||
let mut canvas = Canvas::new(width as u32, height as u32)
|
||||
.ok_or("Screenshot image is too large to encode")?;
|
||||
|
||||
for (frame, (x0, y0, target_width, target_height)) in frames.iter().zip(placement) {
|
||||
if target_width == 0 || target_height == 0 {
|
||||
continue;
|
||||
}
|
||||
let scaled =
|
||||
scale_and_convert(frame, target_width as usize, target_height as usize).ok_or("Invalid capture size")?;
|
||||
canvas.blit(&scaled, target_width as usize, target_height as usize, x0, y0);
|
||||
let scaled = scale_and_convert(frame, target_width as usize, target_height as usize)
|
||||
.ok_or("Invalid capture size")?;
|
||||
canvas.blit(
|
||||
&scaled,
|
||||
target_width as usize,
|
||||
target_height as usize,
|
||||
x0,
|
||||
y0,
|
||||
);
|
||||
}
|
||||
|
||||
Ok(canvas)
|
||||
@@ -269,7 +296,12 @@ fn logical_origin(frames: &[OutputFrame]) -> Option<(i32, i32)> {
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum Geometry {
|
||||
All,
|
||||
Region { x: i32, y: i32, width: i32, height: i32 },
|
||||
Region {
|
||||
x: i32,
|
||||
y: i32,
|
||||
width: i32,
|
||||
height: i32,
|
||||
},
|
||||
}
|
||||
|
||||
impl Geometry {
|
||||
@@ -279,9 +311,16 @@ impl Geometry {
|
||||
if width <= 0 || height <= 0 {
|
||||
return Err("Screenshot region must have a positive size".into());
|
||||
}
|
||||
x.checked_add(width).ok_or("screenshot geometry overflows")?;
|
||||
y.checked_add(height).ok_or("screenshot geometry overflows")?;
|
||||
Ok(Geometry::Region { x, y, width, height })
|
||||
x.checked_add(width)
|
||||
.ok_or("screenshot geometry overflows")?;
|
||||
y.checked_add(height)
|
||||
.ok_or("screenshot geometry overflows")?;
|
||||
Ok(Geometry::Region {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -402,7 +441,8 @@ impl Budget {
|
||||
fn reserve(self: &Arc<Self>, bytes: usize) -> Result<BudgetPermit, String> {
|
||||
self.used
|
||||
.fetch_update(Ordering::AcqRel, Ordering::Acquire, |used| {
|
||||
used.checked_add(bytes).filter(|total| *total <= MAX_PENDING_BYTES)
|
||||
used.checked_add(bytes)
|
||||
.filter(|total| *total <= MAX_PENDING_BYTES)
|
||||
})
|
||||
.map_err(|_| "Screenshot memory budget is exhausted".to_string())?;
|
||||
Ok(BudgetPermit {
|
||||
@@ -492,7 +532,10 @@ impl Coordinator {
|
||||
response_id,
|
||||
reply,
|
||||
state: RequestState::Collecting,
|
||||
parts: specs.into_iter().map(|spec| (spec, PartState::Pending)).collect(),
|
||||
parts: specs
|
||||
.into_iter()
|
||||
.map(|spec| (spec, PartState::Pending))
|
||||
.collect(),
|
||||
received: 0,
|
||||
deadline: now + REQUEST_TIMEOUT,
|
||||
permit: Some(permit),
|
||||
@@ -503,7 +546,9 @@ impl Coordinator {
|
||||
}
|
||||
|
||||
pub(crate) fn permit(&self, request: u64) -> Option<BudgetPermit> {
|
||||
self.requests.get(&request).and_then(|entry| entry.permit.clone())
|
||||
self.requests
|
||||
.get(&request)
|
||||
.and_then(|entry| entry.permit.clone())
|
||||
}
|
||||
|
||||
// Used by the completion tests to assert that a request is still tracked.
|
||||
@@ -519,7 +564,12 @@ impl Coordinator {
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn on_part(&mut self, request: u64, part: usize, result: Result<CaptureBuffer, String>) -> Action {
|
||||
pub(crate) fn on_part(
|
||||
&mut self,
|
||||
request: u64,
|
||||
part: usize,
|
||||
result: Result<CaptureBuffer, String>,
|
||||
) -> Action {
|
||||
let Some(entry) = self.requests.get_mut(&request) else {
|
||||
return Action::None;
|
||||
};
|
||||
@@ -572,7 +622,10 @@ impl Coordinator {
|
||||
Action::Encode {
|
||||
request,
|
||||
frames,
|
||||
permit: entry.permit.take().expect("collecting request owns its reservation"),
|
||||
permit: entry
|
||||
.permit
|
||||
.take()
|
||||
.expect("collecting request owns its reservation"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -613,7 +666,10 @@ impl Coordinator {
|
||||
.map(|(id, _)| *id)
|
||||
.collect();
|
||||
for id in &expired {
|
||||
self.reject_inner(*id, "Screenshot timed out: an output was not redrawn in time");
|
||||
self.reject_inner(
|
||||
*id,
|
||||
"Screenshot timed out: an output was not redrawn in time",
|
||||
);
|
||||
}
|
||||
expired
|
||||
}
|
||||
@@ -728,7 +784,10 @@ impl PlannedPart {
|
||||
// Turns a requested logical region into the per-output reads that cover it.
|
||||
// The logical-to-buffer conversion is shared with the screencopy path so both
|
||||
// agree on rounding at fractional scales.
|
||||
pub(crate) fn plan(geometry: &Geometry, outputs: &[OutputLayout]) -> Result<Vec<PlannedPart>, String> {
|
||||
pub(crate) fn plan(
|
||||
geometry: &Geometry,
|
||||
outputs: &[OutputLayout],
|
||||
) -> Result<Vec<PlannedPart>, String> {
|
||||
if outputs.is_empty() {
|
||||
return Err("No outputs are enabled".into());
|
||||
}
|
||||
@@ -739,7 +798,8 @@ pub(crate) fn plan(geometry: &Geometry, outputs: &[OutputLayout]) -> Result<Vec<
|
||||
{
|
||||
return Err("Screenshot region must have a positive size".into());
|
||||
}
|
||||
let logical_sizes: Vec<Size<i32, Logical>> = outputs.iter().map(OutputLayout::logical_size).collect();
|
||||
let logical_sizes: Vec<Size<i32, Logical>> =
|
||||
outputs.iter().map(OutputLayout::logical_size).collect();
|
||||
|
||||
let requested = match geometry {
|
||||
Geometry::All => {
|
||||
@@ -751,8 +811,10 @@ pub(crate) fn plan(geometry: &Geometry, outputs: &[OutputLayout]) -> Result<Vec<
|
||||
};
|
||||
union = Some(match union {
|
||||
Some(current) => {
|
||||
let top_left =
|
||||
Point::<i32, Logical>::new(current.loc.x.min(rect.loc.x), current.loc.y.min(rect.loc.y));
|
||||
let top_left = Point::<i32, Logical>::new(
|
||||
current.loc.x.min(rect.loc.x),
|
||||
current.loc.y.min(rect.loc.y),
|
||||
);
|
||||
let bottom_right = Point::<i32, Logical>::new(
|
||||
(current.loc.x + current.size.w).max(rect.loc.x + rect.size.w),
|
||||
(current.loc.y + current.size.h).max(rect.loc.y + rect.size.h),
|
||||
@@ -764,7 +826,12 @@ pub(crate) fn plan(geometry: &Geometry, outputs: &[OutputLayout]) -> Result<Vec<
|
||||
}
|
||||
union.expect("outputs is not empty")
|
||||
}
|
||||
Geometry::Region { x, y, width, height } => Rectangle {
|
||||
Geometry::Region {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
} => Rectangle {
|
||||
loc: (*x, *y).into(),
|
||||
size: (*width, *height).into(),
|
||||
},
|
||||
@@ -792,7 +859,11 @@ pub(crate) fn plan(geometry: &Geometry, outputs: &[OutputLayout]) -> Result<Vec<
|
||||
}
|
||||
// capture_region_for_geometry works in output-local logical space.
|
||||
let local = Rectangle {
|
||||
loc: (clipped.loc.x - output.location.x, clipped.loc.y - output.location.y).into(),
|
||||
loc: (
|
||||
clipped.loc.x - output.location.x,
|
||||
clipped.loc.y - output.location.y,
|
||||
)
|
||||
.into(),
|
||||
size: clipped.size,
|
||||
};
|
||||
// The clip above already intersected against this same logical rect, so
|
||||
@@ -839,19 +910,34 @@ mod planning {
|
||||
}
|
||||
|
||||
fn region(x: i32, y: i32, width: i32, height: i32) -> Geometry {
|
||||
Geometry::Region { x, y, width, height }
|
||||
Geometry::Region {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_crop_crossing_a_seam_splits_into_one_part_per_output() {
|
||||
let outputs = [output(0, 0, 1920, 1080, 1.0), output(1920, 0, 1920, 1080, 1.0)];
|
||||
let outputs = [
|
||||
output(0, 0, 1920, 1080, 1.0),
|
||||
output(1920, 0, 1920, 1080, 1.0),
|
||||
];
|
||||
let parts = plan(®ion(1800, 0, 200, 100), &outputs).unwrap();
|
||||
assert_eq!(parts.len(), 2);
|
||||
|
||||
assert_eq!(parts[0].index, 0);
|
||||
assert_eq!((parts[0].logical.loc.x, parts[0].logical.size.w), (1800, 120));
|
||||
assert_eq!(
|
||||
(parts[1].index, parts[1].logical.loc.x, parts[1].logical.size.w),
|
||||
(parts[0].logical.loc.x, parts[0].logical.size.w),
|
||||
(1800, 120)
|
||||
);
|
||||
assert_eq!(
|
||||
(
|
||||
parts[1].index,
|
||||
parts[1].logical.loc.x,
|
||||
parts[1].logical.size.w
|
||||
),
|
||||
(1, 1920, 80)
|
||||
);
|
||||
// The clipped widths must add back up to the request.
|
||||
@@ -884,8 +970,14 @@ mod planning {
|
||||
let outputs = [output(0, 0, 800, 600, 1.0), output(800, 0, 1600, 1200, 1.5)];
|
||||
let parts = plan(®ion(600, 0, 400, 300), &outputs).unwrap();
|
||||
assert_eq!(parts.len(), 2);
|
||||
assert_eq!((parts[0].logical.loc.x, parts[0].logical.size.w), (600, 200));
|
||||
assert_eq!((parts[1].logical.loc.x, parts[1].logical.size.w), (800, 200));
|
||||
assert_eq!(
|
||||
(parts[0].logical.loc.x, parts[0].logical.size.w),
|
||||
(600, 200)
|
||||
);
|
||||
assert_eq!(
|
||||
(parts[1].logical.loc.x, parts[1].logical.size.w),
|
||||
(800, 200)
|
||||
);
|
||||
// The 1.5 output reads back at 1.5x while still reporting logical 200.
|
||||
let spec = parts[1].spec(&outputs[1]);
|
||||
assert_eq!(spec.logical_width, 200);
|
||||
@@ -895,7 +987,10 @@ mod planning {
|
||||
|
||||
#[test]
|
||||
fn all_geometry_unions_every_output() {
|
||||
let outputs = [output(0, 0, 1920, 1080, 1.0), output(-1280, -400, 1280, 800, 1.0)];
|
||||
let outputs = [
|
||||
output(0, 0, 1920, 1080, 1.0),
|
||||
output(-1280, -400, 1280, 800, 1.0),
|
||||
];
|
||||
let parts = plan(&Geometry::All, &outputs).unwrap();
|
||||
assert_eq!(parts.len(), 2);
|
||||
// Neither output is moved into a normalised origin at plan time.
|
||||
@@ -906,7 +1001,10 @@ mod planning {
|
||||
|
||||
#[test]
|
||||
fn a_crop_misses_outputs_that_do_not_intersect() {
|
||||
let outputs = [output(0, 0, 1920, 1080, 1.0), output(1920, 0, 1920, 1080, 1.0)];
|
||||
let outputs = [
|
||||
output(0, 0, 1920, 1080, 1.0),
|
||||
output(1920, 0, 1920, 1080, 1.0),
|
||||
];
|
||||
let parts = plan(®ion(4000, 0, 100, 100), &outputs);
|
||||
assert!(parts.is_err(), "a region off the desktop is reported");
|
||||
}
|
||||
@@ -916,11 +1014,17 @@ mod planning {
|
||||
// The second output starts exactly where the region ends. It contributes
|
||||
// nothing, so the capture must still succeed with just the first output
|
||||
// rather than failing on a region it does not overlap.
|
||||
let outputs = [output(0, 0, 1920, 1080, 1.0), output(1920, 0, 1920, 1080, 1.0)];
|
||||
let outputs = [
|
||||
output(0, 0, 1920, 1080, 1.0),
|
||||
output(1920, 0, 1920, 1080, 1.0),
|
||||
];
|
||||
let parts = plan(®ion(100, 100, 1820, 980), &outputs).expect("captures the overlap");
|
||||
assert_eq!(parts.len(), 1, "only the overlapping output is read");
|
||||
assert_eq!(parts[0].index, 0);
|
||||
assert_eq!((parts[0].logical.size.w, parts[0].logical.size.h), (1820, 980));
|
||||
assert_eq!(
|
||||
(parts[0].logical.size.w, parts[0].logical.size.h),
|
||||
(1820, 980)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -930,10 +1034,16 @@ mod planning {
|
||||
// A 90-degree transform turns the 1920x1080 mode into a 1080x1920
|
||||
// logical area, and the readback follows the same rotation.
|
||||
let parts = plan(®ion(0, 0, 1080, 400), std::slice::from_ref(&rotated)).unwrap();
|
||||
assert_eq!((parts[0].logical.size.w, parts[0].logical.size.h), (1080, 400));
|
||||
assert_eq!(
|
||||
(parts[0].logical.size.w, parts[0].logical.size.h),
|
||||
(1080, 400)
|
||||
);
|
||||
let spec = parts[0].spec(&rotated);
|
||||
assert_eq!(spec.transform, Transform::_90);
|
||||
assert!(spec.buffer_height > spec.buffer_width, "buffer axes are swapped");
|
||||
assert!(
|
||||
spec.buffer_height > spec.buffer_width,
|
||||
"buffer axes are swapped"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1013,9 +1123,17 @@ mod completion {
|
||||
let (reply, received) = sync_channel(8);
|
||||
let mut coordinator = Coordinator::new();
|
||||
coordinator
|
||||
.admit(1, 42, reply.clone(), (0..parts).map(|_| spec(2, 2)).collect())
|
||||
.admit(
|
||||
1,
|
||||
42,
|
||||
reply.clone(),
|
||||
(0..parts).map(|_| spec(2, 2)).collect(),
|
||||
)
|
||||
.expect("admitted");
|
||||
Self { coordinator, received }
|
||||
Self {
|
||||
coordinator,
|
||||
received,
|
||||
}
|
||||
}
|
||||
|
||||
fn response(&self) -> Option<Response> {
|
||||
@@ -1036,7 +1154,11 @@ mod completion {
|
||||
};
|
||||
|
||||
assert_eq!(frames.len(), 1);
|
||||
assert_eq!(frames[0].pixels.as_ptr(), allocation, "readback pixels were copied");
|
||||
assert_eq!(
|
||||
frames[0].pixels.as_ptr(),
|
||||
allocation,
|
||||
"readback pixels were copied"
|
||||
);
|
||||
assert_eq!(frames[0].pixels, vec![137; 16]);
|
||||
}
|
||||
|
||||
@@ -1049,13 +1171,21 @@ mod completion {
|
||||
harness.coordinator.on_part(id, 0, Ok(buffer(2, 2))),
|
||||
Action::None
|
||||
));
|
||||
assert!(harness.coordinator.is_live(id), "still collecting after part 0");
|
||||
assert!(
|
||||
harness.coordinator.is_live(id),
|
||||
"still collecting after part 0"
|
||||
);
|
||||
|
||||
assert!(matches!(
|
||||
harness.coordinator.on_part(id, 1, Err("readback failed".into())),
|
||||
harness
|
||||
.coordinator
|
||||
.on_part(id, 1, Err("readback failed".into())),
|
||||
Action::None
|
||||
));
|
||||
assert!(!harness.coordinator.is_live(id), "one failed part ends the request");
|
||||
assert!(
|
||||
!harness.coordinator.is_live(id),
|
||||
"one failed part ends the request"
|
||||
);
|
||||
|
||||
let response = harness.response().expect("caller is always answered");
|
||||
assert_eq!(response.id, 42);
|
||||
@@ -1070,7 +1200,9 @@ mod completion {
|
||||
|
||||
harness.coordinator.on_part(id, 0, Ok(buffer(2, 2)));
|
||||
// Output 1 is removed from the layout, so its pending capture is failed.
|
||||
harness.coordinator.on_part(id, 1, Err("output was removed".into()));
|
||||
harness
|
||||
.coordinator
|
||||
.on_part(id, 1, Err("output was removed".into()));
|
||||
assert!(!harness.coordinator.is_live(id));
|
||||
assert!(harness.response().unwrap().error.is_some());
|
||||
|
||||
@@ -1109,14 +1241,26 @@ mod completion {
|
||||
fn admission_enforces_one_budget_across_requests_and_late_readbacks() {
|
||||
let (reply, _received) = sync_channel(8);
|
||||
let mut coordinator = Coordinator::new();
|
||||
let first = coordinator.admit(1, 1, reply.clone(), vec![spec(4096, 8192)]).unwrap();
|
||||
let second = coordinator.admit(1, 2, reply.clone(), vec![spec(4096, 8192)]).unwrap();
|
||||
assert!(coordinator.admit(1, 3, reply.clone(), vec![spec(2, 2)]).is_err());
|
||||
let first = coordinator
|
||||
.admit(1, 1, reply.clone(), vec![spec(4096, 8192)])
|
||||
.unwrap();
|
||||
let second = coordinator
|
||||
.admit(1, 2, reply.clone(), vec![spec(4096, 8192)])
|
||||
.unwrap();
|
||||
assert!(
|
||||
coordinator
|
||||
.admit(1, 3, reply.clone(), vec![spec(2, 2)])
|
||||
.is_err()
|
||||
);
|
||||
|
||||
// A cancelled request can still have a readback retained by a sink or channel.
|
||||
let late_readback = coordinator.permit(first).unwrap();
|
||||
coordinator.reject(first, "cancelled");
|
||||
assert!(coordinator.admit(1, 4, reply.clone(), vec![spec(2, 2)]).is_err());
|
||||
assert!(
|
||||
coordinator
|
||||
.admit(1, 4, reply.clone(), vec![spec(2, 2)])
|
||||
.is_err()
|
||||
);
|
||||
drop(late_readback);
|
||||
assert!(coordinator.admit(1, 5, reply, vec![spec(2, 2)]).is_ok());
|
||||
coordinator.reject(second, "finished");
|
||||
@@ -1127,19 +1271,48 @@ mod completion {
|
||||
let mut harness = Harness::new(1);
|
||||
let id = harness.coordinator.next_id;
|
||||
let bytes = harness.coordinator.budget.used.load(Ordering::Acquire);
|
||||
let Action::Encode { frames, permit, .. } = harness.coordinator.on_part(id, 0, Ok(buffer(2, 2))) else {
|
||||
let Action::Encode { frames, permit, .. } =
|
||||
harness.coordinator.on_part(id, 0, Ok(buffer(2, 2)))
|
||||
else {
|
||||
panic!("ready request should encode");
|
||||
};
|
||||
harness.coordinator.reject(id, "cancelled while encoder owns pixels");
|
||||
assert_eq!(harness.coordinator.budget.used.load(Ordering::Acquire), bytes);
|
||||
assert!(harness.coordinator.budget.reserve(MAX_PENDING_BYTES).is_err());
|
||||
harness
|
||||
.coordinator
|
||||
.reject(id, "cancelled while encoder owns pixels");
|
||||
assert_eq!(
|
||||
harness.coordinator.budget.used.load(Ordering::Acquire),
|
||||
bytes
|
||||
);
|
||||
assert!(
|
||||
harness
|
||||
.coordinator
|
||||
.budget
|
||||
.reserve(MAX_PENDING_BYTES)
|
||||
.is_err()
|
||||
);
|
||||
drop(frames);
|
||||
drop(permit);
|
||||
assert_eq!(harness.coordinator.budget.used.load(Ordering::Acquire), 0);
|
||||
let first = harness.coordinator.budget.reserve(MAX_PENDING_BYTES / 2 + 1).unwrap();
|
||||
assert!(harness.coordinator.budget.reserve(MAX_PENDING_BYTES / 2).is_err());
|
||||
let first = harness
|
||||
.coordinator
|
||||
.budget
|
||||
.reserve(MAX_PENDING_BYTES / 2 + 1)
|
||||
.unwrap();
|
||||
assert!(
|
||||
harness
|
||||
.coordinator
|
||||
.budget
|
||||
.reserve(MAX_PENDING_BYTES / 2)
|
||||
.is_err()
|
||||
);
|
||||
drop(first);
|
||||
assert!(harness.coordinator.budget.reserve(MAX_PENDING_BYTES).is_ok());
|
||||
assert!(
|
||||
harness
|
||||
.coordinator
|
||||
.budget
|
||||
.reserve(MAX_PENDING_BYTES)
|
||||
.is_ok()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1152,7 +1325,10 @@ mod completion {
|
||||
Action::None
|
||||
));
|
||||
let action = harness.coordinator.on_part(id, 1, Ok(buffer(2, 2)));
|
||||
let Action::Encode { request, frames, .. } = action else {
|
||||
let Action::Encode {
|
||||
request, frames, ..
|
||||
} = action
|
||||
else {
|
||||
panic!("expected the request to move to encoding");
|
||||
};
|
||||
assert_eq!(request, id);
|
||||
@@ -1177,7 +1353,9 @@ mod completion {
|
||||
// Unlocking does not resurrect the request; the file the worker
|
||||
// eventually produces is ours to delete.
|
||||
assert!(!harness.coordinator.is_live(id));
|
||||
let action = harness.coordinator.on_encoded(id, Ok(PathBuf::from("/tmp/late.png")));
|
||||
let action = harness
|
||||
.coordinator
|
||||
.on_encoded(id, Ok(PathBuf::from("/tmp/late.png")));
|
||||
assert!(matches!(action, Action::DiscardFile(ref path) if path.ends_with("late.png")));
|
||||
assert!(harness.response().is_none(), "no late success is delivered");
|
||||
}
|
||||
@@ -1189,7 +1367,9 @@ mod completion {
|
||||
harness.coordinator.on_part(id, 0, Ok(buffer(2, 2)));
|
||||
assert!(harness.coordinator.is_encoding(id));
|
||||
assert_eq!(
|
||||
harness.coordinator.terminate_all_with_reason("Capture privacy changed"),
|
||||
harness
|
||||
.coordinator
|
||||
.terminate_all_with_reason("Capture privacy changed"),
|
||||
vec![id]
|
||||
);
|
||||
assert!(harness.response().unwrap().error.is_some());
|
||||
@@ -1208,7 +1388,9 @@ mod completion {
|
||||
let id = harness.coordinator.next_id;
|
||||
harness.coordinator.on_part(id, 0, Ok(buffer(2, 2)));
|
||||
|
||||
harness.coordinator.on_encoded(id, Ok(PathBuf::from("/tmp/shot.png")));
|
||||
harness
|
||||
.coordinator
|
||||
.on_encoded(id, Ok(PathBuf::from("/tmp/shot.png")));
|
||||
let response = harness.response().expect("success is answered");
|
||||
assert_eq!(response.id, 42);
|
||||
assert!(response.error.is_none());
|
||||
@@ -1216,7 +1398,9 @@ mod completion {
|
||||
|
||||
// A duplicate completion for the same request has nowhere to go, and
|
||||
// its file is an orphan rather than a second delivery.
|
||||
let action = harness.coordinator.on_encoded(id, Ok(PathBuf::from("/tmp/again.png")));
|
||||
let action = harness
|
||||
.coordinator
|
||||
.on_encoded(id, Ok(PathBuf::from("/tmp/again.png")));
|
||||
assert!(matches!(action, Action::DiscardFile(_)));
|
||||
|
||||
// A delivered path is reported separately so the caller cannot
|
||||
@@ -1239,7 +1423,9 @@ mod completion {
|
||||
let id = harness.coordinator.next_id;
|
||||
harness.coordinator.on_part(id, 0, Ok(buffer(2, 2)));
|
||||
|
||||
harness.coordinator.on_encoded(id, Err("encoding failed".into()));
|
||||
harness
|
||||
.coordinator
|
||||
.on_encoded(id, Err("encoding failed".into()));
|
||||
let response = harness.response().expect("failure is answered");
|
||||
assert_eq!(response.error.unwrap().message, "encoding failed");
|
||||
assert!(!harness.coordinator.is_live(id));
|
||||
@@ -1252,13 +1438,23 @@ mod completion {
|
||||
// client would never be answered.
|
||||
let mut harness = Harness::new(2);
|
||||
let id = harness.coordinator.next_id;
|
||||
harness.coordinator.requests.get_mut(&id).expect("tracked").deadline = Instant::now() - Duration::from_secs(1);
|
||||
harness
|
||||
.coordinator
|
||||
.requests
|
||||
.get_mut(&id)
|
||||
.expect("tracked")
|
||||
.deadline = Instant::now() - Duration::from_secs(1);
|
||||
|
||||
assert_eq!(harness.coordinator.expire_at(Instant::now()), vec![id]);
|
||||
let response = harness.response().expect("the caller is answered");
|
||||
assert_eq!(response.error.as_ref().unwrap().code, "screenshot_failed");
|
||||
assert!(
|
||||
response.error.as_ref().unwrap().message.contains("timed out"),
|
||||
response
|
||||
.error
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.message
|
||||
.contains("timed out"),
|
||||
"the failure explains itself: {:?}",
|
||||
response.error.as_ref().unwrap().message
|
||||
);
|
||||
@@ -1275,7 +1471,10 @@ mod completion {
|
||||
fn a_live_request_is_not_expired_early() {
|
||||
let mut harness = Harness::new(1);
|
||||
let id = harness.coordinator.next_id;
|
||||
assert!(harness.coordinator.expire_at(Instant::now()).is_empty(), "not yet due");
|
||||
assert!(
|
||||
harness.coordinator.expire_at(Instant::now()).is_empty(),
|
||||
"not yet due"
|
||||
);
|
||||
assert!(harness.coordinator.is_live(id));
|
||||
assert!(harness.response().is_none(), "still unanswered, not failed");
|
||||
let remaining = harness
|
||||
@@ -1334,7 +1533,9 @@ mod completion {
|
||||
);
|
||||
}
|
||||
|
||||
fn observe_deadlines(coordinator: &mut Coordinator) -> std::rc::Rc<std::cell::RefCell<Vec<Option<Instant>>>> {
|
||||
fn observe_deadlines(
|
||||
coordinator: &mut Coordinator,
|
||||
) -> std::rc::Rc<std::cell::RefCell<Vec<Option<Instant>>>> {
|
||||
let changes = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
|
||||
let observed = changes.clone();
|
||||
coordinator.set_deadline_observer(move |deadline| observed.borrow_mut().push(deadline));
|
||||
@@ -1356,15 +1557,29 @@ mod completion {
|
||||
.admit_at(1, 42, reply.clone(), vec![spec(2, 2)], late)
|
||||
.unwrap();
|
||||
let second = coordinator
|
||||
.admit_at(2, 43, reply.clone(), vec![spec(2, 2)], late + Duration::from_secs(5))
|
||||
.admit_at(
|
||||
2,
|
||||
43,
|
||||
reply.clone(),
|
||||
vec![spec(2, 2)],
|
||||
late + Duration::from_secs(5),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
changes.borrow().len(),
|
||||
2,
|
||||
"a later request does not rearm the earliest timer"
|
||||
);
|
||||
assert!(coordinator.admit_at(1, 44, reply, Vec::new(), late).is_err());
|
||||
assert_eq!(changes.borrow().len(), 2, "rejected admission does not touch the timer");
|
||||
assert!(
|
||||
coordinator
|
||||
.admit_at(1, 44, reply, Vec::new(), late)
|
||||
.is_err()
|
||||
);
|
||||
assert_eq!(
|
||||
changes.borrow().len(),
|
||||
2,
|
||||
"rejected admission does not touch the timer"
|
||||
);
|
||||
assert_eq!(coordinator.terminate_owner(1), vec![first]);
|
||||
assert!(coordinator.is_live(second));
|
||||
assert_eq!(coordinator.terminate_owner(99), Vec::<u64>::new());
|
||||
@@ -1382,7 +1597,10 @@ mod completion {
|
||||
|
||||
#[test]
|
||||
fn deadline_survives_readback_and_encoding_then_disarms_on_completion() {
|
||||
for result in [Ok(PathBuf::from("/tmp/shot.png")), Err("encoder failed".into())] {
|
||||
for result in [
|
||||
Ok(PathBuf::from("/tmp/shot.png")),
|
||||
Err("encoder failed".into()),
|
||||
] {
|
||||
let mut harness = Harness::new(2);
|
||||
let changes = observe_deadlines(&mut harness.coordinator);
|
||||
let id = harness.coordinator.next_id;
|
||||
@@ -1392,11 +1610,19 @@ mod completion {
|
||||
harness.coordinator.on_part(id, 1, Ok(buffer(2, 2))),
|
||||
Action::Encode { .. }
|
||||
));
|
||||
assert_eq!(&*changes.borrow(), &[deadline], "encoding keeps the original deadline");
|
||||
assert_eq!(
|
||||
&*changes.borrow(),
|
||||
&[deadline],
|
||||
"encoding keeps the original deadline"
|
||||
);
|
||||
harness.coordinator.on_encoded(id, result);
|
||||
assert_eq!(&*changes.borrow(), &[deadline, None]);
|
||||
harness.coordinator.on_part(id, 0, Err("late readback".into()));
|
||||
harness.coordinator.on_encoded(id, Err("late encode".into()));
|
||||
harness
|
||||
.coordinator
|
||||
.on_part(id, 0, Err("late readback".into()));
|
||||
harness
|
||||
.coordinator
|
||||
.on_encoded(id, Err("late encode".into()));
|
||||
assert_eq!(
|
||||
changes.borrow().len(),
|
||||
2,
|
||||
@@ -1440,7 +1666,11 @@ mod completion {
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
assert_eq!(changes.borrow().last(), Some(&None), "{cancellation} must disarm");
|
||||
assert_eq!(
|
||||
changes.borrow().last(),
|
||||
Some(&None),
|
||||
"{cancellation} must disarm"
|
||||
);
|
||||
assert_eq!(changes.borrow().len(), 2);
|
||||
assert!(harness.response().unwrap().error.is_some());
|
||||
assert!(!harness.coordinator.is_live(id));
|
||||
@@ -1458,7 +1688,13 @@ mod completion {
|
||||
.admit_at(1, 42, reply.clone(), vec![spec(2, 2)], now)
|
||||
.unwrap();
|
||||
let second = coordinator
|
||||
.admit_at(2, 43, reply.clone(), vec![spec(2, 2)], now + Duration::from_secs(5))
|
||||
.admit_at(
|
||||
2,
|
||||
43,
|
||||
reply.clone(),
|
||||
vec![spec(2, 2)],
|
||||
now + Duration::from_secs(5),
|
||||
)
|
||||
.unwrap();
|
||||
coordinator.on_part(first, 0, Ok(buffer(2, 2)));
|
||||
assert!(
|
||||
@@ -1487,7 +1723,13 @@ mod completion {
|
||||
Action::DiscardFile(_)
|
||||
));
|
||||
coordinator
|
||||
.admit_at(3, 44, reply, vec![spec(2, 2)], now + Duration::from_secs(3600))
|
||||
.admit_at(
|
||||
3,
|
||||
44,
|
||||
reply,
|
||||
vec![spec(2, 2)],
|
||||
now + Duration::from_secs(3600),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
changes.borrow().last(),
|
||||
@@ -1505,7 +1747,10 @@ mod completion {
|
||||
assert!(!sink.publish(buffer(64, 64)));
|
||||
|
||||
let PartOutcome {
|
||||
request, part, result, ..
|
||||
request,
|
||||
part,
|
||||
result,
|
||||
..
|
||||
} = receiver.try_recv().expect("published once");
|
||||
assert_eq!((request, part), (7, 1));
|
||||
assert_eq!(result.unwrap().width, 2);
|
||||
@@ -1522,7 +1767,10 @@ mod completion {
|
||||
assert!(!sink.publish(buffer(2, 2)));
|
||||
|
||||
let PartOutcome {
|
||||
request, part, result, ..
|
||||
request,
|
||||
part,
|
||||
result,
|
||||
..
|
||||
} = receiver.try_recv().expect("answered");
|
||||
assert_eq!((request, part), (3, 0));
|
||||
assert!(result.is_err());
|
||||
@@ -1913,7 +2161,11 @@ mod tests {
|
||||
// 9 logical at scale 1.5.
|
||||
assert_eq!((canvas.width, canvas.height), (14, 5));
|
||||
for column in 0..canvas.width as usize {
|
||||
assert_eq!(canvas.pixels[column * 4 + 3], 255, "column {column} has a gap");
|
||||
assert_eq!(
|
||||
canvas.pixels[column * 4 + 3],
|
||||
255,
|
||||
"column {column} has a gap"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1945,7 +2197,11 @@ mod tests {
|
||||
|
||||
let canvas = compose(&[left, right]).unwrap();
|
||||
assert_eq!(canvas.width, 5);
|
||||
assert_eq!(&canvas.pixels[2 * 4..3 * 4], &[0, 0, 0, 0], "gap is transparent");
|
||||
assert_eq!(
|
||||
&canvas.pixels[2 * 4..3 * 4],
|
||||
&[0, 0, 0, 0],
|
||||
"gap is transparent"
|
||||
);
|
||||
assert_eq!(canvas.pixels[3], 255);
|
||||
assert_eq!(canvas.pixels[3 * 4 + 3], 255);
|
||||
}
|
||||
@@ -1974,7 +2230,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn geometry_tolerates_surrounding_whitespace_from_slurp() {
|
||||
for raw in ["100,200 800x600\n", " 100,200 800x600 ", "100,200 800x600\r\n"] {
|
||||
for raw in [
|
||||
"100,200 800x600\n",
|
||||
" 100,200 800x600 ",
|
||||
"100,200 800x600\r\n",
|
||||
] {
|
||||
assert_eq!(
|
||||
parse_geometry(raw).unwrap(),
|
||||
Geometry::Region {
|
||||
|
||||
@@ -113,7 +113,8 @@ fn encode_png(canvas: &Canvas) -> Result<Vec<u8>, String> {
|
||||
if width == 0 || height == 0 {
|
||||
return Err("Screenshot is empty".into());
|
||||
}
|
||||
let image = image::ImageBuffer::<image::Rgba<u8>, _>::from_raw(width, height, canvas.pixels.as_slice())
|
||||
let image =
|
||||
image::ImageBuffer::<image::Rgba<u8>, _>::from_raw(width, height, canvas.pixels.as_slice())
|
||||
.ok_or("Screenshot buffer does not match its dimensions")?;
|
||||
|
||||
let mut out = Vec::new();
|
||||
@@ -122,7 +123,13 @@ fn encode_png(canvas: &Canvas) -> Result<Vec<u8>, String> {
|
||||
image::codecs::png::CompressionType::Fast,
|
||||
image::codecs::png::FilterType::Adaptive,
|
||||
);
|
||||
image::ImageEncoder::write_image(encoder, image.as_raw(), width, height, image::ExtendedColorType::Rgba8)
|
||||
image::ImageEncoder::write_image(
|
||||
encoder,
|
||||
image.as_raw(),
|
||||
width,
|
||||
height,
|
||||
image::ExtendedColorType::Rgba8,
|
||||
)
|
||||
.map_err(|error| format!("Could not encode screenshot: {error}"))?;
|
||||
tracing::debug!(encoded = out.len(), "encoded screenshot");
|
||||
Ok(out)
|
||||
@@ -178,8 +185,8 @@ fn write_private_file_in(
|
||||
}
|
||||
|
||||
fn private_directory() -> Result<PathBuf, String> {
|
||||
let runtime =
|
||||
std::env::var_os("XDG_RUNTIME_DIR").ok_or("XDG_RUNTIME_DIR is not set, so screenshots cannot be staged")?;
|
||||
let runtime = std::env::var_os("XDG_RUNTIME_DIR")
|
||||
.ok_or("XDG_RUNTIME_DIR is not set, so screenshots cannot be staged")?;
|
||||
let root = PathBuf::from(runtime);
|
||||
let directory = root.join("ferese-screenshots");
|
||||
match fs::symlink_metadata(&directory) {
|
||||
@@ -194,7 +201,9 @@ fn private_directory() -> Result<PathBuf, String> {
|
||||
}
|
||||
Err(error) if error.kind() == io::ErrorKind::NotFound => {
|
||||
fs::create_dir(&directory)
|
||||
.and_then(|()| fs::set_permissions(&directory, fs::Permissions::from_mode(DIR_MODE)))
|
||||
.and_then(|()| {
|
||||
fs::set_permissions(&directory, fs::Permissions::from_mode(DIR_MODE))
|
||||
})
|
||||
.map_err(|error| format!("Could not create screenshot directory: {error}"))?;
|
||||
}
|
||||
Err(error) => return Err(format!("Could not inspect screenshot directory: {error}")),
|
||||
@@ -248,7 +257,11 @@ mod tests {
|
||||
.expect("decodes back")
|
||||
.to_rgba8();
|
||||
assert_eq!(decoded.dimensions(), (4, 3));
|
||||
assert_eq!(decoded.as_raw(), &canvas.pixels, "encoding changed the pixel data");
|
||||
assert_eq!(
|
||||
decoded.as_raw(),
|
||||
&canvas.pixels,
|
||||
"encoding changed the pixel data"
|
||||
);
|
||||
// The opaque alpha we force is preserved through the round trip.
|
||||
assert_eq!(decoded.get_pixel(0, 0)[3], 255);
|
||||
}
|
||||
@@ -279,7 +292,10 @@ mod tests {
|
||||
let path = write_private_file(&png).expect("staged");
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let mode = fs::metadata(&path).unwrap().permissions().mode() & 0o777;
|
||||
assert_eq!(mode, FILE_MODE, "staged screenshots are not group/world readable");
|
||||
assert_eq!(
|
||||
mode, FILE_MODE,
|
||||
"staged screenshots are not group/world readable"
|
||||
);
|
||||
fs::remove_file(&path).unwrap();
|
||||
}
|
||||
|
||||
@@ -293,7 +309,10 @@ mod tests {
|
||||
fs::write(&other, b"not a screenshot").unwrap();
|
||||
|
||||
sweep_stale_files();
|
||||
assert!(fresh.exists(), "a freshly staged screenshot is still in use");
|
||||
assert!(
|
||||
fresh.exists(),
|
||||
"a freshly staged screenshot is still in use"
|
||||
);
|
||||
assert!(other.exists(), "unrelated files are left alone");
|
||||
|
||||
fs::remove_file(&fresh).unwrap();
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use smithay::reexports::wayland_server::Client;
|
||||
use smithay::wayland::security_context::{SecurityContext, SecurityContextHandler, SecurityContextListenerSource};
|
||||
use smithay::wayland::security_context::{
|
||||
SecurityContext, SecurityContextHandler, SecurityContextListenerSource,
|
||||
};
|
||||
|
||||
use crate::Ferese;
|
||||
use crate::state::ClientState;
|
||||
@@ -14,7 +16,9 @@ pub(crate) fn unrestricted_client(client: &Client) -> bool {
|
||||
|
||||
impl SecurityContextHandler for Ferese {
|
||||
fn context_created(&mut self, source: SecurityContextListenerSource, context: SecurityContext) {
|
||||
if let Err(error) = self.loop_handle.insert_source(source, move |stream, _, state| {
|
||||
if let Err(error) = self
|
||||
.loop_handle
|
||||
.insert_source(source, move |stream, _, state| {
|
||||
let data = ClientState {
|
||||
security_context: Some(context.clone()),
|
||||
..Default::default()
|
||||
@@ -22,7 +26,8 @@ impl SecurityContextHandler for Ferese {
|
||||
if let Err(error) = state.display_handle.insert_client(stream, Arc::new(data)) {
|
||||
tracing::warn!(%error, "failed to register sandboxed Wayland client");
|
||||
}
|
||||
}) {
|
||||
})
|
||||
{
|
||||
tracing::warn!(%error, "failed to register security context listener");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,19 +28,28 @@ fn icon_cleanup_messages(destroy_icon: bool) -> Vec<(u32, u16, Vec<u32>)> {
|
||||
.create_resource::<WlCompositor, GlobalData, Ferese>(&state.display_handle, 6, GlobalData)
|
||||
.unwrap();
|
||||
let shell = client
|
||||
.create_resource::<XdgWmBase, XdgWmBaseUserData, Ferese>(&state.display_handle, 6, Default::default())
|
||||
.create_resource::<XdgWmBase, XdgWmBaseUserData, Ferese>(
|
||||
&state.display_handle,
|
||||
6,
|
||||
Default::default(),
|
||||
)
|
||||
.unwrap();
|
||||
let shm = client
|
||||
.create_resource::<WlShm, GlobalData, Ferese>(&state.display_handle, 1, GlobalData)
|
||||
.unwrap();
|
||||
let manager = client
|
||||
.create_resource::<XdgToplevelIconManagerV1, GlobalData, Ferese>(&state.display_handle, 1, GlobalData)
|
||||
.create_resource::<XdgToplevelIconManagerV1, GlobalData, Ferese>(
|
||||
&state.display_handle,
|
||||
1,
|
||||
GlobalData,
|
||||
)
|
||||
.unwrap();
|
||||
request(&mut wire, compositor.id().protocol_id(), 0, &[2], None);
|
||||
request(&mut wire, shell.id().protocol_id(), 2, &[3, 2], None);
|
||||
request(&mut wire, 3, 1, &[4], None);
|
||||
let mut file = tempfile::tempfile().unwrap();
|
||||
file.write_all(&0xff112233u32.to_ne_bytes().repeat(32 * 32)).unwrap();
|
||||
file.write_all(&0xff112233u32.to_ne_bytes().repeat(32 * 32))
|
||||
.unwrap();
|
||||
request(
|
||||
&mut wire,
|
||||
shm.id().protocol_id(),
|
||||
@@ -85,9 +94,15 @@ fn destroying_an_assigned_icon_before_its_buffer_keeps_the_client_connected() {
|
||||
return;
|
||||
}
|
||||
let messages = icon_cleanup_messages(true);
|
||||
assert!(!messages.iter().any(|(object, opcode, _)| *object == 1 && *opcode == 0));
|
||||
assert!(
|
||||
messages.iter().any(|(object, opcode, _)| *object == 8 && *opcode == 0),
|
||||
!messages
|
||||
.iter()
|
||||
.any(|(object, opcode, _)| *object == 1 && *opcode == 0)
|
||||
);
|
||||
assert!(
|
||||
messages
|
||||
.iter()
|
||||
.any(|(object, opcode, _)| *object == 8 && *opcode == 0),
|
||||
"valid icon cleanup disconnected the client before its sync reply: {messages:?}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,9 @@ use smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::zwlr_scre
|
||||
use smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::zwlr_screencopy_frame_v1::ZwlrScreencopyFrameV1;
|
||||
use smithay::reexports::wayland_server::protocol::wl_buffer::WlBuffer;
|
||||
use smithay::reexports::wayland_server::protocol::wl_shm;
|
||||
use smithay::reexports::wayland_server::{Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource};
|
||||
use smithay::reexports::wayland_server::{
|
||||
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
|
||||
};
|
||||
use smithay::utils::Rectangle;
|
||||
use smithay::wayland::shm::with_buffer_contents_mut;
|
||||
|
||||
@@ -34,7 +36,10 @@ struct Permit {
|
||||
impl Permit {
|
||||
fn reserve(budget: &Arc<Mutex<Budget>>, bytes: usize) -> Option<Self> {
|
||||
let mut available = budget.lock().unwrap();
|
||||
if bytes == 0 || bytes > MAX_BYTES.saturating_sub(available.bytes) || available.frames >= MAX_FRAMES {
|
||||
if bytes == 0
|
||||
|| bytes > MAX_BYTES.saturating_sub(available.bytes)
|
||||
|| available.frames >= MAX_FRAMES
|
||||
{
|
||||
return None;
|
||||
}
|
||||
available.bytes += bytes;
|
||||
@@ -77,7 +82,11 @@ struct Conversion {
|
||||
|
||||
pub(crate) fn is_portal(stream: &std::os::unix::net::UnixStream) -> bool {
|
||||
use std::os::fd::AsRawFd;
|
||||
let mut credentials = libc::ucred { pid: 0, uid: 0, gid: 0 };
|
||||
let mut credentials = libc::ucred {
|
||||
pid: 0,
|
||||
uid: 0,
|
||||
gid: 0,
|
||||
};
|
||||
let mut length = std::mem::size_of_val(&credentials) as libc::socklen_t;
|
||||
// SAFETY: the connected socket is live and both out pointers cover their
|
||||
// initialized stack allocations. SO_PEERCRED identifies the connecting process.
|
||||
@@ -95,7 +104,10 @@ pub(crate) fn is_portal(stream: &std::os::unix::net::UnixStream) -> bool {
|
||||
}
|
||||
let expected = std::env::current_exe()
|
||||
.ok()
|
||||
.and_then(|exe| exe.parent().map(|dir| dir.join("xdg-desktop-portal-ferese")))
|
||||
.and_then(|exe| {
|
||||
exe.parent()
|
||||
.map(|dir| dir.join("xdg-desktop-portal-ferese"))
|
||||
})
|
||||
.and_then(|exe| std::fs::metadata(exe).ok());
|
||||
let actual = std::fs::metadata(format!("/proc/{}/exe", credentials.pid)).ok();
|
||||
matches!((expected, actual), (Some(expected), Some(actual)) if expected.dev() == actual.dev() && expected.ino() == actual.ino())
|
||||
@@ -178,7 +190,9 @@ fn finish_conversion(state: &Ferese, result: Conversion) {
|
||||
|
||||
impl GlobalDispatch<FereseWindowCaptureManagerV1, Global> for Ferese {
|
||||
fn can_view(client: Client, _: &Global) -> bool {
|
||||
client.get_data::<crate::state::ClientState>().is_some_and(|state| {
|
||||
client
|
||||
.get_data::<crate::state::ClientState>()
|
||||
.is_some_and(|state| {
|
||||
state
|
||||
.capabilities
|
||||
.contains(crate::private_client::ClientCapabilities::WINDOW_CAPTURE)
|
||||
@@ -248,7 +262,12 @@ impl Dispatch<FereseWindowCaptureManagerV1, Global> for Ferese {
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
*resource.data::<Arc<FrameData>>().unwrap().used.lock().unwrap() = true;
|
||||
*resource
|
||||
.data::<Arc<FrameData>>()
|
||||
.unwrap()
|
||||
.used
|
||||
.lock()
|
||||
.unwrap() = true;
|
||||
manager.busy(&resource);
|
||||
resource.failed();
|
||||
}
|
||||
@@ -287,7 +306,8 @@ impl Ferese {
|
||||
.clone();
|
||||
let geometry = window.geometry();
|
||||
let scale = output.current_scale().fractional_scale();
|
||||
let size: smithay::utils::Size<i32, smithay::utils::Physical> = geometry.size.to_physical_precise_round(scale);
|
||||
let size: smithay::utils::Size<i32, smithay::utils::Physical> =
|
||||
geometry.size.to_physical_precise_round(scale);
|
||||
let bytes = usize::try_from(size.w)
|
||||
.ok()?
|
||||
.checked_mul(usize::try_from(size.h).ok()?)?
|
||||
@@ -299,7 +319,9 @@ impl Ferese {
|
||||
*busy = true;
|
||||
return None;
|
||||
};
|
||||
let cursor = if overlay_cursor && !self.overview.is_presenting() && !self.capture_cursor_protected() {
|
||||
let cursor =
|
||||
if overlay_cursor && !self.overview.is_presenting() && !self.capture_cursor_protected()
|
||||
{
|
||||
self.seat.get_pointer().and_then(|pointer| {
|
||||
let location = pointer.current_location();
|
||||
if self.window_under_visual(location).as_ref() != Some(&window) {
|
||||
@@ -307,7 +329,8 @@ impl Ferese {
|
||||
}
|
||||
let (x, y) = self.inverse_presented_window_point(id, location.x, location.y)?;
|
||||
let surface_point = smithay::utils::Point::from((x, y)) + geometry.loc.to_f64();
|
||||
let (surface, _) = window.surface_under(surface_point, smithay::desktop::WindowSurfaceType::ALL)?;
|
||||
let (surface, _) = window
|
||||
.surface_under(surface_point, smithay::desktop::WindowSurfaceType::ALL)?;
|
||||
if pointer.current_focus().as_ref() != Some(&surface) {
|
||||
return None;
|
||||
}
|
||||
@@ -357,10 +380,18 @@ impl Ferese {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn copy_snapshot(state: &Ferese, resource: &ZwlrScreencopyFrameV1, data: &FrameData, buffer: &WlBuffer) {
|
||||
pub(super) fn copy_snapshot(
|
||||
state: &Ferese,
|
||||
resource: &ZwlrScreencopyFrameV1,
|
||||
data: &FrameData,
|
||||
buffer: &WlBuffer,
|
||||
) {
|
||||
let mut used = data.used.lock().unwrap();
|
||||
if *used {
|
||||
resource.post_error(frame::Error::AlreadyUsed, "window capture frame has already been used");
|
||||
resource.post_error(
|
||||
frame::Error::AlreadyUsed,
|
||||
"window capture frame has already been used",
|
||||
);
|
||||
return;
|
||||
}
|
||||
*used = true;
|
||||
@@ -393,7 +424,11 @@ pub(super) fn copy_snapshot(state: &Ferese, resource: &ZwlrScreencopyFrameV1, da
|
||||
// SAFETY: Smithay owns the writable mapping, validated format/stride and
|
||||
// checked offset cover the complete snapshot; the source is separately owned.
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(snapshot.pixels.pixels.as_ptr(), destination.add(offset), bytes);
|
||||
std::ptr::copy_nonoverlapping(
|
||||
snapshot.pixels.pixels.as_ptr(),
|
||||
destination.add(offset),
|
||||
bytes,
|
||||
);
|
||||
}
|
||||
true
|
||||
})
|
||||
@@ -459,7 +494,11 @@ pub(crate) mod tests {
|
||||
.unwrap();
|
||||
let data = frame_data();
|
||||
let frame = client
|
||||
.create_resource::<ZwlrScreencopyFrameV1, Arc<FrameData>, Ferese>(&state.display_handle, 3, data.clone())
|
||||
.create_resource::<ZwlrScreencopyFrameV1, Arc<FrameData>, Ferese>(
|
||||
&state.display_handle,
|
||||
3,
|
||||
data.clone(),
|
||||
)
|
||||
.unwrap();
|
||||
finish_conversion(
|
||||
state,
|
||||
@@ -475,20 +514,27 @@ pub(crate) mod tests {
|
||||
let data = frame_data();
|
||||
*data.snapshot.lock().unwrap() = Some(snapshot());
|
||||
let frame = client
|
||||
.create_resource::<ZwlrScreencopyFrameV1, Arc<FrameData>, Ferese>(&state.display_handle, 3, data.clone())
|
||||
.create_resource::<ZwlrScreencopyFrameV1, Arc<FrameData>, Ferese>(
|
||||
&state.display_handle,
|
||||
3,
|
||||
data.clone(),
|
||||
)
|
||||
.unwrap();
|
||||
let buffer = client
|
||||
.object_from_protocol_id::<WlBuffer>(&state.display_handle, 6)
|
||||
.unwrap();
|
||||
let before = with_buffer_contents_mut(&buffer, |ptr, _, layout| {
|
||||
// SAFETY: the fixture allocates and validates this 64x48 SHM buffer.
|
||||
unsafe { std::slice::from_raw_parts(ptr.add(layout.offset as usize), 64 * 48 * 4).to_vec() }
|
||||
unsafe {
|
||||
std::slice::from_raw_parts(ptr.add(layout.offset as usize), 64 * 48 * 4).to_vec()
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
copy_snapshot(state, &frame, &data, &buffer);
|
||||
with_buffer_contents_mut(&buffer, |ptr, _, layout| {
|
||||
// SAFETY: same fixture-owned buffer as above.
|
||||
let after = unsafe { std::slice::from_raw_parts(ptr.add(layout.offset as usize), 64 * 48 * 4) };
|
||||
let after =
|
||||
unsafe { std::slice::from_raw_parts(ptr.add(layout.offset as usize), 64 * 48 * 4) };
|
||||
if allowed {
|
||||
assert!(after.iter().all(|byte| *byte == 0));
|
||||
} else {
|
||||
@@ -496,7 +542,11 @@ pub(crate) mod tests {
|
||||
}
|
||||
// Restore the client's colored buffer for subsequent renderer checks.
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(before.as_ptr(), ptr.add(layout.offset as usize), before.len());
|
||||
std::ptr::copy_nonoverlapping(
|
||||
before.as_ptr(),
|
||||
ptr.add(layout.offset as usize),
|
||||
before.len(),
|
||||
);
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
@@ -60,7 +60,9 @@ impl XdgShellHandler for Ferese {
|
||||
fn parent_changed(&mut self, surface: ToplevelSurface) {
|
||||
self.refresh_capture_privacy();
|
||||
self.refresh_idle_inhibition();
|
||||
let parent = surface.parent().and_then(|parent| self.windows.id_for_surface(&parent));
|
||||
let parent = surface
|
||||
.parent()
|
||||
.and_then(|parent| self.windows.id_for_surface(&parent));
|
||||
let child = self
|
||||
.windows
|
||||
.id_for_surface(surface.wl_surface())
|
||||
@@ -95,7 +97,12 @@ impl XdgShellHandler for Ferese {
|
||||
}
|
||||
}
|
||||
|
||||
fn reposition_request(&mut self, surface: PopupSurface, positioner: PositionerState, token: u32) {
|
||||
fn reposition_request(
|
||||
&mut self,
|
||||
surface: PopupSurface,
|
||||
positioner: PositionerState,
|
||||
token: u32,
|
||||
) {
|
||||
surface.with_pending_state(|state| {
|
||||
state.geometry = positioner.get_geometry();
|
||||
state.positioner = positioner;
|
||||
@@ -252,7 +259,12 @@ impl XdgShellHandler for Ferese {
|
||||
};
|
||||
|
||||
if let Some(pointer) = seat.get_pointer() {
|
||||
pointer.set_grab(self, PopupPointerGrab::new(&popup_grab), serial, Focus::Keep);
|
||||
pointer.set_grab(
|
||||
self,
|
||||
PopupPointerGrab::new(&popup_grab),
|
||||
serial,
|
||||
Focus::Keep,
|
||||
);
|
||||
}
|
||||
if let Some(keyboard) = seat.get_keyboard() {
|
||||
keyboard.set_grab(self, PopupKeyboardGrab::new(&popup_grab), serial);
|
||||
@@ -264,14 +276,21 @@ impl XdgShellHandler for Ferese {
|
||||
}
|
||||
}
|
||||
|
||||
fn check_grab(seat: &Seat<Ferese>, surface: &WlSurface, serial: Serial) -> Option<GrabStartData<Ferese>> {
|
||||
fn check_grab(
|
||||
seat: &Seat<Ferese>,
|
||||
surface: &WlSurface,
|
||||
serial: Serial,
|
||||
) -> Option<GrabStartData<Ferese>> {
|
||||
let pointer = seat.get_pointer()?;
|
||||
if !pointer.has_grab(serial) {
|
||||
return None;
|
||||
}
|
||||
let start_data = pointer.grab_start_data()?;
|
||||
let (focus, _) = start_data.focus.as_ref()?;
|
||||
focus.id().same_client_as(&surface.id()).then_some(start_data)
|
||||
focus
|
||||
.id()
|
||||
.same_client_as(&surface.id())
|
||||
.then_some(start_data)
|
||||
}
|
||||
|
||||
impl XdgDecorationHandler for Ferese {
|
||||
@@ -325,12 +344,14 @@ pub(crate) fn validate_size_constraints(toplevel: &ToplevelSurface) -> bool {
|
||||
let mut cached = states.cached_state.get::<SurfaceCachedState>();
|
||||
let state = cached.current();
|
||||
let valid_axis = |min: i32, max: i32| min >= 0 && max >= 0 && (max == 0 || min <= max);
|
||||
valid_axis(state.min_size.w, state.max_size.w) && valid_axis(state.min_size.h, state.max_size.h)
|
||||
valid_axis(state.min_size.w, state.max_size.w)
|
||||
&& valid_axis(state.min_size.h, state.max_size.h)
|
||||
});
|
||||
if !valid {
|
||||
toplevel
|
||||
.xdg_toplevel()
|
||||
.post_error(xdg_toplevel::Error::InvalidSize, "invalid minimum/maximum window size");
|
||||
toplevel.xdg_toplevel().post_error(
|
||||
xdg_toplevel::Error::InvalidSize,
|
||||
"invalid minimum/maximum window size",
|
||||
);
|
||||
}
|
||||
|
||||
valid
|
||||
@@ -359,7 +380,9 @@ pub fn handle_commit(state: &mut Ferese, surface: &WlSurface) {
|
||||
if let Some(PopupKind::Xdg(popup)) = state.popups.find_popup(surface)
|
||||
&& !popup.is_initial_configure_sent()
|
||||
{
|
||||
popup.send_configure().expect("initial popup configure is valid");
|
||||
popup
|
||||
.send_configure()
|
||||
.expect("initial popup configure is valid");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -415,10 +438,11 @@ impl Ferese {
|
||||
let Ok(root) = find_popup_root_surface(&PopupKind::Xdg(popup.clone())) else {
|
||||
return;
|
||||
};
|
||||
let window = self
|
||||
.space
|
||||
.elements()
|
||||
.find(|window| window.toplevel().is_some_and(|toplevel| toplevel.wl_surface() == &root));
|
||||
let window = self.space.elements().find(|window| {
|
||||
window
|
||||
.toplevel()
|
||||
.is_some_and(|toplevel| toplevel.wl_surface() == &root)
|
||||
});
|
||||
let geometry = (|| {
|
||||
if let Some(window) = window {
|
||||
let id = *self.windows.ids().get(window)?;
|
||||
@@ -432,11 +456,14 @@ impl Ferese {
|
||||
|
||||
Some((output_geometry, location))
|
||||
} else {
|
||||
let layer = self.space.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL)?;
|
||||
let layer = self
|
||||
.space
|
||||
.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL)?;
|
||||
|
||||
self.space.outputs().find_map(|output| {
|
||||
let output_geometry = self.space.output_geometry(output)?;
|
||||
let layer_geometry = smithay::desktop::layer_map_for_output(output).layer_geometry(&layer)?;
|
||||
let layer_geometry =
|
||||
smithay::desktop::layer_map_for_output(output).layer_geometry(&layer)?;
|
||||
|
||||
Some((output_geometry, output_geometry.loc + layer_geometry.loc))
|
||||
})
|
||||
@@ -490,7 +517,9 @@ impl Dispatch<xdg_toplevel::XdgToplevel, XdgShellSurfaceUserData> for Ferese {
|
||||
return;
|
||||
}
|
||||
|
||||
smithay::wayland::Dispatch2::request(data, state, client, resource, request, dhandle, data_init);
|
||||
smithay::wayland::Dispatch2::request(
|
||||
data, state, client, resource, request, dhandle, data_init,
|
||||
);
|
||||
}
|
||||
|
||||
fn destroyed(
|
||||
@@ -524,7 +553,11 @@ fn size_limits_usable(
|
||||
(pending.max_size.w, pending.max_size.h),
|
||||
)
|
||||
});
|
||||
let (min, max) = if minimum { (proposed, max) } else { (min, proposed) };
|
||||
let (min, max) = if minimum {
|
||||
(proposed, max)
|
||||
} else {
|
||||
(min, proposed)
|
||||
};
|
||||
(max.0 == 0 || min.0 <= max.0) && (max.1 == 0 || min.1 <= max.1)
|
||||
}
|
||||
|
||||
|
||||
@@ -77,16 +77,21 @@ fn player_window(player: &media::Player, windows: &[AppWindow<'_>]) -> Option<Wi
|
||||
impl Ferese {
|
||||
pub(crate) fn refresh_idle_inhibition(&mut self) {
|
||||
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
|
||||
.window_rules
|
||||
.iter()
|
||||
.any(|rule| rule.idle_policy().is_some_and(|mode| mode != Mode::None));
|
||||
let playback_policy = self.idle_inhibit.fullscreen_playback
|
||||
|| self
|
||||
.window_rules
|
||||
.iter()
|
||||
.any(|rule| matches!(rule.idle_policy(), Some(Mode::Playing | Mode::FullscreenPlaying)));
|
||||
|| self.window_rules.iter().any(|rule| {
|
||||
matches!(
|
||||
rule.idle_policy(),
|
||||
Some(Mode::Playing | Mode::FullscreenPlaying)
|
||||
)
|
||||
});
|
||||
let automatic = (playback_policy && playing) || selected;
|
||||
let eligible = if self.idle_inhibitors.is_empty() && !automatic {
|
||||
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",
|
||||
).unwrap().runtime_config().unwrap();
|
||||
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));
|
||||
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!(crate::window_rules::live_result(visible, none, false), None);
|
||||
for mode in ["playing", "fullscreen", "fullscreen-playing", "none"] {
|
||||
@@ -211,9 +222,15 @@ mod tests {
|
||||
);
|
||||
}
|
||||
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]
|
||||
@@ -236,7 +253,10 @@ mod tests {
|
||||
pid: Some(99),
|
||||
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, &[]), None);
|
||||
assert_eq!(
|
||||
|
||||
@@ -57,7 +57,8 @@ impl Engine {
|
||||
self.locked = locked;
|
||||
if locked {
|
||||
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 {
|
||||
let _ = response.try_send(Response::error(id, "session_locked", "Session is locked"));
|
||||
} else if since != self.snapshot.revision {
|
||||
@@ -98,8 +105,15 @@ impl Engine {
|
||||
self.waiters.remove(&owner);
|
||||
}
|
||||
|
||||
pub(crate) fn action(&self, args: Value, reply: Option<(u64, mpsc::SyncSender<Response>)>) -> Result<(), String> {
|
||||
let commands = self.commands.as_ref().ok_or("Media service is unavailable")?;
|
||||
pub(crate) fn action(
|
||||
&self,
|
||||
args: Value,
|
||||
reply: Option<(u64, mpsc::SyncSender<Response>)>,
|
||||
) -> Result<(), String> {
|
||||
let commands = self
|
||||
.commands
|
||||
.as_ref()
|
||||
.ok_or("Media service is unavailable")?;
|
||||
commands
|
||||
.try_send(Command {
|
||||
args,
|
||||
@@ -113,7 +127,9 @@ impl Engine {
|
||||
pub(crate) fn init(event_loop: &mut EventLoop<Ferese>) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let (sender, receiver) = sync_channel::<Update>(1);
|
||||
let (commands, receive_commands) = async_channel::bounded(16);
|
||||
event_loop.handle().insert_source(receiver, move |event, _, state| {
|
||||
event_loop
|
||||
.handle()
|
||||
.insert_source(receiver, move |event, _, state| {
|
||||
if let Event::Msg(update) = event {
|
||||
let changed = inhibition_changed(&state.media_players, &update.players);
|
||||
state.media_players = update.players;
|
||||
|
||||
@@ -38,7 +38,11 @@ async fn bus_call<T: serde::de::DeserializeOwned + zbus::zvariant::Type>(
|
||||
.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
|
||||
.call_method(Some(owner), PATH, Some(PROPERTIES), "GetAll", &(interface,))
|
||||
.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> {
|
||||
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 {
|
||||
@@ -163,7 +169,11 @@ async fn discover(connection: &Connection, name: String, owner: String) -> Playe
|
||||
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")?;
|
||||
if kind == "auto" {
|
||||
state.pinned = None;
|
||||
@@ -176,7 +186,10 @@ async fn action(connection: &Connection, state: &mut PlayersState, args: &serde_
|
||||
}
|
||||
.cloned()
|
||||
.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());
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
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
|
||||
.length_us
|
||||
.map_or(position, |length| position.min(length))
|
||||
.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
|
||||
.call_method(
|
||||
Some(player.owner.as_str()),
|
||||
@@ -279,7 +295,11 @@ async fn action(connection: &Connection, state: &mut PlayersState, args: &serde_
|
||||
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();
|
||||
if previous
|
||||
.as_ref()
|
||||
@@ -336,7 +356,10 @@ async fn monitor(
|
||||
.deserialize()?;
|
||||
let mut names: Vec<_> = names
|
||||
.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();
|
||||
names.sort_unstable();
|
||||
for name in names.into_iter().take(MAX_PLAYERS) {
|
||||
@@ -367,7 +390,9 @@ async fn monitor(
|
||||
};
|
||||
match input {
|
||||
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)) => {
|
||||
let result = if command.sent.elapsed() > Duration::from_secs(2) {
|
||||
Err("Media command expired".into())
|
||||
@@ -389,8 +414,12 @@ async fn monitor(
|
||||
.member()
|
||||
.is_some_and(|member| member.as_str() == "NameOwnerChanged")
|
||||
{
|
||||
let (name, _, owner): (String, String, String) = message.body().deserialize()?;
|
||||
if !name.strip_prefix(PREFIX).is_some_and(|suffix| suffix.starts_with('.')) {
|
||||
let (name, _, owner): (String, String, String) =
|
||||
message.body().deserialize()?;
|
||||
if !name
|
||||
.strip_prefix(PREFIX)
|
||||
.is_some_and(|suffix| suffix.starts_with('.'))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
state.remove(&name);
|
||||
@@ -402,7 +431,10 @@ async fn monitor(
|
||||
state.update(player);
|
||||
}
|
||||
} 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
|
||||
.players
|
||||
.values()
|
||||
@@ -417,14 +449,17 @@ async fn monitor(
|
||||
}
|
||||
} else {
|
||||
let Ok((interface, changed, invalidated)) =
|
||||
message.body().deserialize::<(String, Properties, Vec<String>)>()
|
||||
message
|
||||
.body()
|
||||
.deserialize::<(String, Properties, Vec<String>)>()
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let identity = interface == PREFIX
|
||||
&& ["Identity", "DesktopEntry", "CanRaise"]
|
||||
.iter()
|
||||
.any(|key| changed.contains_key(*key) || invalidated.iter().any(|field| field == key));
|
||||
&& ["Identity", "DesktopEntry", "CanRaise"].iter().any(|key| {
|
||||
changed.contains_key(*key)
|
||||
|| invalidated.iter().any(|field| field == key)
|
||||
});
|
||||
let playback = interface == PLAYER
|
||||
&& [
|
||||
"PlaybackStatus",
|
||||
@@ -439,7 +474,10 @@ async fn monitor(
|
||||
"CanControl",
|
||||
]
|
||||
.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 {
|
||||
continue;
|
||||
}
|
||||
@@ -483,10 +521,16 @@ pub(super) fn run(sender: SyncSender<Update>, commands: async_channel::Receiver<
|
||||
if matches!(result, Ok(())) || commands.is_closed() {
|
||||
break;
|
||||
}
|
||||
if sender.send(PlayersState::default().update_snapshot()).is_err() {
|
||||
if sender
|
||||
.send(PlayersState::default().update_snapshot())
|
||||
.is_err()
|
||||
{
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -47,8 +47,9 @@ impl PlayersState {
|
||||
}
|
||||
|
||||
pub fn selected(&self) -> Option<&Player> {
|
||||
let eligible =
|
||||
|player: &&Player| player.view.status != Playback::Stopped && !self.ignored.contains(&player.name);
|
||||
let eligible = |player: &&Player| {
|
||||
player.view.status != Playback::Stopped && !self.ignored.contains(&player.name)
|
||||
};
|
||||
if let Some(player) = self
|
||||
.pinned
|
||||
.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() {
|
||||
let mut state = PlayersState::default();
|
||||
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");
|
||||
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] {
|
||||
let (send, receive) = mpsc::sync_channel(1);
|
||||
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);
|
||||
let (send, receive) = mpsc::sync_channel(1);
|
||||
|
||||
+136
-50
@@ -1,13 +1,14 @@
|
||||
use ferese_layout::Direction;
|
||||
use smithay::backend::input::{
|
||||
AbsolutePositionEvent, Axis, AxisSource, ButtonState, Event, GestureBeginEvent, GestureEndEvent,
|
||||
GestureSwipeUpdateEvent as _, InputBackend, InputEvent, KeyState, KeyboardKeyEvent, PointerAxisEvent,
|
||||
PointerButtonEvent, PointerMotionEvent, Switch, SwitchState, SwitchToggleEvent, TouchEvent,
|
||||
AbsolutePositionEvent, Axis, AxisSource, ButtonState, Event, GestureBeginEvent,
|
||||
GestureEndEvent, GestureSwipeUpdateEvent as _, InputBackend, InputEvent, KeyState,
|
||||
KeyboardKeyEvent, PointerAxisEvent, PointerButtonEvent, PointerMotionEvent, Switch,
|
||||
SwitchState, SwitchToggleEvent, TouchEvent,
|
||||
};
|
||||
use smithay::input::keyboard::{FilterResult, keysyms};
|
||||
use smithay::input::pointer::{
|
||||
AxisFrame, ButtonEvent, Focus, GestureSwipeBeginEvent, GestureSwipeEndEvent, GestureSwipeUpdateEvent,
|
||||
GrabStartData, MotionEvent, PointerHandle, RelativeMotionEvent,
|
||||
AxisFrame, ButtonEvent, Focus, GestureSwipeBeginEvent, GestureSwipeEndEvent,
|
||||
GestureSwipeUpdateEvent, GrabStartData, MotionEvent, PointerHandle, RelativeMotionEvent,
|
||||
};
|
||||
use smithay::input::touch::{DownEvent, MotionEvent as TouchMotionEvent, UpEvent};
|
||||
use smithay::reexports::wayland_server::Resource;
|
||||
@@ -63,7 +64,8 @@ impl Ferese {
|
||||
let layers = smithay::desktop::layer_map_for_output(output);
|
||||
|
||||
for layer in layers.layers() {
|
||||
let Some(local) = surface_tree_position(layer.wl_surface(), surface, (0, 0).into()) else {
|
||||
let Some(local) = surface_tree_position(layer.wl_surface(), surface, (0, 0).into())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let geometry = layers.layer_geometry(layer)?;
|
||||
@@ -135,7 +137,9 @@ impl Ferese {
|
||||
|
||||
if !matches!(
|
||||
&event,
|
||||
InputEvent::DeviceAdded { .. } | InputEvent::DeviceRemoved { .. } | InputEvent::SwitchToggle { .. }
|
||||
InputEvent::DeviceAdded { .. }
|
||||
| InputEvent::DeviceRemoved { .. }
|
||||
| InputEvent::SwitchToggle { .. }
|
||||
) {
|
||||
self.lock_input_activity();
|
||||
}
|
||||
@@ -221,14 +225,19 @@ impl Ferese {
|
||||
}
|
||||
BindingAction::Focus(focus @ (Direction::Left | Direction::Right)) => {
|
||||
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);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
seat.get_pointer().expect("seat has a pointer").gesture_swipe_update(
|
||||
seat.get_pointer()
|
||||
.expect("seat has a pointer")
|
||||
.gesture_swipe_update(
|
||||
self,
|
||||
&GestureSwipeUpdateEvent {
|
||||
time: event.time(),
|
||||
@@ -241,12 +250,14 @@ impl Ferese {
|
||||
if self.swipe.active() {
|
||||
let fingers = self.swipe.fingers();
|
||||
let cancelled = event.cancelled() || self.swipe_navigation_blocked();
|
||||
let direction = self
|
||||
.swipe
|
||||
.finish_at(cancelled, std::time::Duration::from_micros(event.time().micros()));
|
||||
let direction = self.swipe.finish_at(
|
||||
cancelled,
|
||||
std::time::Duration::from_micros(event.time().micros()),
|
||||
);
|
||||
let workspace_handled = self.finish_workspace_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
|
||||
&& let Some(direction) = direction
|
||||
&& let Some(action) = self
|
||||
@@ -256,7 +267,9 @@ impl Ferese {
|
||||
.map(|binding| binding.action.clone())
|
||||
{
|
||||
match action {
|
||||
BindingAction::Focus(direction @ (Direction::Left | Direction::Right)) => {
|
||||
BindingAction::Focus(
|
||||
direction @ (Direction::Left | Direction::Right),
|
||||
) => {
|
||||
self.focus_direction_from_swipe(direction);
|
||||
}
|
||||
BindingAction::SwitchRelativeWorkspace(next) => {
|
||||
@@ -266,7 +279,9 @@ impl Ferese {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
seat.get_pointer().expect("seat has a pointer").gesture_swipe_end(
|
||||
seat.get_pointer()
|
||||
.expect("seat has a pointer")
|
||||
.gesture_swipe_end(
|
||||
self,
|
||||
&GestureSwipeEndEvent {
|
||||
serial: SERIAL_COUNTER.next_serial(),
|
||||
@@ -544,7 +559,8 @@ impl Ferese {
|
||||
focus,
|
||||
&RelativeMotionEvent {
|
||||
delta: (delta.x / scale_x, delta.y / scale_y).into(),
|
||||
delta_unaccel: (delta_unaccel.x / scale_x, delta_unaccel.y / scale_y).into(),
|
||||
delta_unaccel: (delta_unaccel.x / scale_x, delta_unaccel.y / scale_y)
|
||||
.into(),
|
||||
time: event.time(),
|
||||
},
|
||||
);
|
||||
@@ -574,7 +590,8 @@ impl Ferese {
|
||||
}
|
||||
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;
|
||||
}
|
||||
@@ -733,12 +750,12 @@ impl Ferese {
|
||||
InputEvent::PointerAxis { event, .. } => {
|
||||
self.last_pointer_time = event.time();
|
||||
let source = event.source();
|
||||
let horizontal = event
|
||||
.amount(Axis::Horizontal)
|
||||
.unwrap_or_else(|| event.amount_v120(Axis::Horizontal).unwrap_or(0.0) * 15.0 / 120.0);
|
||||
let vertical = event
|
||||
.amount(Axis::Vertical)
|
||||
.unwrap_or_else(|| event.amount_v120(Axis::Vertical).unwrap_or(0.0) * 15.0 / 120.0);
|
||||
let horizontal = event.amount(Axis::Horizontal).unwrap_or_else(|| {
|
||||
event.amount_v120(Axis::Horizontal).unwrap_or(0.0) * 15.0 / 120.0
|
||||
});
|
||||
let vertical = event.amount(Axis::Vertical).unwrap_or_else(|| {
|
||||
event.amount_v120(Axis::Vertical).unwrap_or(0.0) * 15.0 / 120.0
|
||||
});
|
||||
if self.input_capture.active() {
|
||||
if self.input_capture.captures(2) {
|
||||
self.input_capture.push(serde_json::json!({"type":"scroll", "x":horizontal, "y":vertical, "v120_x":event.amount_v120(Axis::Horizontal), "v120_y":event.amount_v120(Axis::Vertical), "stop_x":source == AxisSource::Finger && event.amount(Axis::Horizontal) == Some(0.0), "stop_y":source == AxisSource::Finger && event.amount(Axis::Vertical) == Some(0.0)}));
|
||||
@@ -748,13 +765,21 @@ impl Ferese {
|
||||
let pointer = self.seat.get_pointer().expect("seat has a pointer");
|
||||
if self.scroll_tab_indicator(
|
||||
pointer.current_location(),
|
||||
if vertical != 0.0 { vertical } else { horizontal },
|
||||
if vertical != 0.0 {
|
||||
vertical
|
||||
} else {
|
||||
horizontal
|
||||
},
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if self.scroll_overview_strip(
|
||||
pointer.current_location(),
|
||||
if horizontal != 0.0 { horizontal } else { vertical },
|
||||
if horizontal != 0.0 {
|
||||
horizontal
|
||||
} else {
|
||||
vertical
|
||||
},
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -791,7 +816,8 @@ impl Ferese {
|
||||
let Some(location) = self.absolute_event_position(&event) else {
|
||||
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());
|
||||
return;
|
||||
}
|
||||
@@ -800,7 +826,10 @@ impl Ferese {
|
||||
let serial = SERIAL_COUNTER.next_serial();
|
||||
|
||||
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(
|
||||
@@ -865,7 +894,11 @@ impl Ferese {
|
||||
if self.session_lock.active() || self.input_capture.active() {
|
||||
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;
|
||||
};
|
||||
if self
|
||||
@@ -890,7 +923,12 @@ impl Ferese {
|
||||
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()
|
||||
|| pointer.is_grabbed()
|
||||
|| !matches!(button, BTN_LEFT | BTN_RIGHT)
|
||||
@@ -956,7 +994,9 @@ impl Ferese {
|
||||
I: InputBackend,
|
||||
E: AbsolutePositionEvent<I>,
|
||||
{
|
||||
let output = self.focused_output().or_else(|| self.space.outputs().next())?;
|
||||
let output = self
|
||||
.focused_output()
|
||||
.or_else(|| self.space.outputs().next())?;
|
||||
let geometry = self.space.output_geometry(output)?;
|
||||
|
||||
Some(event.position_transformed(geometry.size) + geometry.loc.to_f64())
|
||||
@@ -965,8 +1005,14 @@ impl Ferese {
|
||||
fn swipe_navigation_blocked(&self) -> bool {
|
||||
self.session_lock.active()
|
||||
|| self.active_shortcuts_inhibitor.is_some()
|
||||
|| self.seat.get_pointer().is_some_and(|pointer| pointer.is_grabbed())
|
||||
|| self.seat.get_keyboard().is_some_and(|keyboard| keyboard.is_grabbed())
|
||||
|| self
|
||||
.seat
|
||||
.get_pointer()
|
||||
.is_some_and(|pointer| pointer.is_grabbed())
|
||||
|| self
|
||||
.seat
|
||||
.get_keyboard()
|
||||
.is_some_and(|keyboard| keyboard.is_grabbed())
|
||||
}
|
||||
|
||||
fn switch_relative_workspace(&mut self, next: bool, slide: Option<SwipeDirection>) {
|
||||
@@ -982,7 +1028,9 @@ impl Ferese {
|
||||
pub(crate) fn relative_workspace_target(&self, next: bool) -> Option<ferese_core::WorkspaceId> {
|
||||
let output = self.output_workspaces.focused_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 neighbor = if next {
|
||||
index.checked_add(1)?
|
||||
@@ -1009,7 +1057,9 @@ impl Ferese {
|
||||
if self.session_lock.active() {
|
||||
// Repeats retain press ownership and never run configurable commands
|
||||
// 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(
|
||||
self,
|
||||
["brightnessctl", "set", step],
|
||||
@@ -1087,7 +1137,8 @@ impl Ferese {
|
||||
{
|
||||
// The client may enable OnDemand only after receiving this
|
||||
// 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;
|
||||
}
|
||||
@@ -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() {
|
||||
return;
|
||||
}
|
||||
@@ -1193,7 +1248,10 @@ impl Ferese {
|
||||
}
|
||||
// Keep the explicitly selected window focused until Overview's exit
|
||||
// animation settles. Moving previews must not steal focus on pointer jitter.
|
||||
if self.overview.is_presenting() || !self.input_settings.focus_follows_mouse || pointer.is_grabbed() {
|
||||
if self.overview.is_presenting()
|
||||
|| !self.input_settings.focus_follows_mouse
|
||||
|| pointer.is_grabbed()
|
||||
{
|
||||
return;
|
||||
}
|
||||
if self.layer_under(position).is_some() {
|
||||
@@ -1323,9 +1381,11 @@ impl Ferese {
|
||||
BindingAction::None => {}
|
||||
BindingAction::BindingMode(name) => self.set_binding_mode(&name),
|
||||
BindingAction::Spawn(argv) => {
|
||||
if let Some(child) =
|
||||
spawn_client(self, argv.iter(), crate::private_client::ClientCapabilities::default())
|
||||
{
|
||||
if let Some(child) = spawn_client(
|
||||
self,
|
||||
argv.iter(),
|
||||
crate::private_client::ClientCapabilities::default(),
|
||||
) {
|
||||
crate::process::watch_client_exit(&self.loop_handle, child);
|
||||
}
|
||||
}
|
||||
@@ -1337,7 +1397,9 @@ impl Ferese {
|
||||
BindingAction::FocusMru(reverse) => self.cycle_focus(reverse, false),
|
||||
BindingAction::Move(direction) => self.move_direction(direction),
|
||||
BindingAction::Resize(direction) => self.resize_direction(direction),
|
||||
BindingAction::SwitchRelativeWorkspace(next) => self.switch_relative_workspace(next, None),
|
||||
BindingAction::SwitchRelativeWorkspace(next) => {
|
||||
self.switch_relative_workspace(next, None)
|
||||
}
|
||||
BindingAction::WorkspaceBackAndForth => self.workspace_back_and_forth(),
|
||||
BindingAction::SwitchWorkspace(workspace) => {
|
||||
self.switch_workspace_from_binding(u32::from(workspace));
|
||||
@@ -1359,7 +1421,10 @@ impl Ferese {
|
||||
BindingAction::ToggleFloating => self.toggle_focused_floating(),
|
||||
BindingAction::ToggleOverview => self.toggle_overview(),
|
||||
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");
|
||||
}
|
||||
}
|
||||
@@ -1474,17 +1539,25 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
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;
|
||||
}
|
||||
|
||||
let mut events = smithay::reexports::calloop::EventLoop::try_new().unwrap();
|
||||
let mut state = crate::startup_tests::state(&mut events);
|
||||
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 {
|
||||
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)
|
||||
@@ -1494,8 +1567,15 @@ mod tests {
|
||||
.collect();
|
||||
assert_eq!(children.len(), 24);
|
||||
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(3);
|
||||
while !std::fs::read_to_string(&children_path).unwrap().trim().is_empty() {
|
||||
assert!(std::time::Instant::now() < deadline, "binding children were not reaped");
|
||||
while !std::fs::read_to_string(&children_path)
|
||||
.unwrap()
|
||||
.trim()
|
||||
.is_empty()
|
||||
{
|
||||
assert!(
|
||||
std::time::Instant::now() < deadline,
|
||||
"binding children were not reaped"
|
||||
);
|
||||
events
|
||||
.dispatch(Some(std::time::Duration::from_millis(20)), &mut state)
|
||||
.unwrap();
|
||||
@@ -1504,8 +1584,14 @@ mod tests {
|
||||
|
||||
for pid in children {
|
||||
let mut status = 0;
|
||||
assert_eq!(unsafe { libc::waitpid(pid, &mut status, libc::WNOHANG) }, -1);
|
||||
assert_eq!(std::io::Error::last_os_error().raw_os_error(), Some(libc::ECHILD));
|
||||
assert_eq!(
|
||||
unsafe { libc::waitpid(pid, &mut status, libc::WNOHANG) },
|
||||
-1
|
||||
);
|
||||
assert_eq!(
|
||||
std::io::Error::last_os_error().raw_os_error(),
|
||||
Some(libc::ECHILD)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -60,7 +60,11 @@ pub(crate) fn key(state: &mut Ferese, code: u32, pressed: bool) {
|
||||
state.process_input_event::<TestInput>(InputEvent::Keyboard {
|
||||
event: TestKey(
|
||||
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;
|
||||
let mut state = Ferese::new(events, Display::new().unwrap(), config).unwrap();
|
||||
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");
|
||||
state.config_sections = Some(sections);
|
||||
state
|
||||
@@ -149,7 +156,11 @@ fn reload_transitions_cancel_modes_without_losing_key_ownership() {
|
||||
let mut state = state(&mut events);
|
||||
state.set_binding_mode("resize");
|
||||
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!(state.intercepted_keys.contains(&Keycode::new(43)));
|
||||
state
|
||||
@@ -163,7 +174,11 @@ fn reload_transitions_cancel_modes_without_losing_key_ownership() {
|
||||
state
|
||||
.reload_config_source(format!("{SOURCE}\ninput {{ repeat-rate 30; }}"))
|
||||
.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
|
||||
.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]
|
||||
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;
|
||||
}
|
||||
let mut events = EventLoop::try_new().unwrap();
|
||||
@@ -220,7 +236,9 @@ fn input_capture_exits_modes_and_prevents_reentry() {
|
||||
y: 0,
|
||||
}]);
|
||||
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
|
||||
.input_capture
|
||||
.set_barriers(
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use super::*;
|
||||
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::output::{Mode, Output, PhysicalProperties, Subpixel};
|
||||
use smithay::reexports::calloop::EventLoop;
|
||||
@@ -11,7 +13,11 @@ use std::os::fd::AsRawFd;
|
||||
use std::os::unix::net::UnixStream;
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -81,7 +87,9 @@ fn layer_with_policy(
|
||||
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());
|
||||
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;
|
||||
}
|
||||
@@ -93,7 +101,8 @@ fn layer_with_policy(
|
||||
"acknowledging configure without mapping must not acquire focus"
|
||||
);
|
||||
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(
|
||||
&mut wire,
|
||||
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);
|
||||
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);
|
||||
dispatch(&mut events, &mut state);
|
||||
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 first_id = state.windows.ids()[&first];
|
||||
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.));
|
||||
state.input_settings.focus_follows_mouse = true;
|
||||
keyboard.set_focus(
|
||||
@@ -297,7 +312,12 @@ fn fixture_with_animations(animations: bool) -> (Ferese, EventLoop<'static, Fere
|
||||
(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, 2, 6, &[], None);
|
||||
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, 6, &[], None);
|
||||
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!(
|
||||
state
|
||||
.space
|
||||
.layer_for_surface(panel.wl_surface(), smithay::desktop::WindowSurfaceType::TOPLEVEL)
|
||||
.layer_for_surface(
|
||||
panel.wl_surface(),
|
||||
smithay::desktop::WindowSurfaceType::TOPLEVEL
|
||||
)
|
||||
.is_some()
|
||||
);
|
||||
assert!(
|
||||
smithay::backend::renderer::utils::with_renderer_surface_state(panel.wl_surface(), |s| s
|
||||
.buffer()
|
||||
.is_none())
|
||||
smithay::backend::renderer::utils::with_renderer_surface_state(
|
||||
panel.wl_surface(),
|
||||
|s| s.buffer().is_none()
|
||||
)
|
||||
.unwrap()
|
||||
);
|
||||
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.
|
||||
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 header = u32::from_ne_bytes(bytes[offset + 4..offset + 8].try_into().unwrap());
|
||||
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;
|
||||
}
|
||||
@@ -495,7 +528,11 @@ fn layer_mapping_commits_do_not_override_session_lock() {
|
||||
request(&mut wire, 2, 6, &[], None);
|
||||
dispatch(&mut events, &mut state);
|
||||
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);
|
||||
dispatch(&mut events, &mut state);
|
||||
assert_eq!(
|
||||
@@ -631,7 +668,11 @@ fn valid_layer_popup_grab_survives_commits_but_not_parent_unmapping() {
|
||||
)
|
||||
.unwrap();
|
||||
let shell = client
|
||||
.create_resource::<XdgWmBase, XdgWmBaseUserData, Ferese>(&state.display_handle, 6, Default::default())
|
||||
.create_resource::<XdgWmBase, XdgWmBaseUserData, Ferese>(
|
||||
&state.display_handle,
|
||||
6,
|
||||
Default::default(),
|
||||
)
|
||||
.unwrap();
|
||||
request(&mut wire, shell.id().protocol_id(), 1, &[6], None); // Positioner.
|
||||
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 grab = state
|
||||
.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();
|
||||
let keyboard = state.seat.get_keyboard().unwrap();
|
||||
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, 6, &[], None);
|
||||
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(), 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);
|
||||
let (top, _top_wire) = layer_with_policy(&mut state, &mut events, &output, 1, 2);
|
||||
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(&output, smithay::wayland::shell::wlr_layer::Layer::Top, true));
|
||||
assert!(!state.layer_visible_for_frame(
|
||||
&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);
|
||||
assert!(state.layer_keyboard_eligible(&top));
|
||||
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);
|
||||
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);
|
||||
dispatch(&mut events, &mut state);
|
||||
assert_eq!(
|
||||
|
||||
@@ -89,14 +89,24 @@ fn edge(barrier: &Barrier, zones: &[Zone]) -> Option<Edge> {
|
||||
}
|
||||
match found {
|
||||
Edge::Top | Edge::Bottom => {
|
||||
let outside = y1 + if matches!(found, Edge::Top) { -0.5 } else { 0.5 };
|
||||
let outside = y1
|
||||
+ if matches!(found, Edge::Top) {
|
||||
-0.5
|
||||
} else {
|
||||
0.5
|
||||
};
|
||||
outside >= f64::from(other.y)
|
||||
&& outside < other.bottom()
|
||||
&& x1 < other.right()
|
||||
&& x2 + 1.0 > f64::from(other.x)
|
||||
}
|
||||
Edge::Left | Edge::Right => {
|
||||
let outside = x1 + if matches!(found, Edge::Left) { -0.5 } else { 0.5 };
|
||||
let outside = x1
|
||||
+ if matches!(found, Edge::Left) {
|
||||
-0.5
|
||||
} else {
|
||||
0.5
|
||||
};
|
||||
outside >= f64::from(other.x)
|
||||
&& outside < other.right()
|
||||
&& y1 < other.bottom()
|
||||
@@ -174,7 +184,10 @@ impl InputCapture {
|
||||
if self.sessions.len() >= MAX_SESSIONS || capabilities == 0 || capabilities & !3 != 0 {
|
||||
return Err("Unsupported capabilities or input capture session limit reached".into());
|
||||
}
|
||||
self.next_session = self.next_session.checked_add(1).ok_or("Input capture IDs exhausted")?;
|
||||
self.next_session = self
|
||||
.next_session
|
||||
.checked_add(1)
|
||||
.ok_or("Input capture IDs exhausted")?;
|
||||
let id = self.next_session;
|
||||
self.sessions.insert(
|
||||
id,
|
||||
@@ -193,15 +206,24 @@ impl InputCapture {
|
||||
|
||||
pub(crate) fn zones(&self, id: u64) -> Result<Value, String> {
|
||||
self.session(id)?;
|
||||
Ok(json!({"zone_set":self.zone_set, "zones":self.zones.iter().map(|zone|
|
||||
(zone.width, zone.height, zone.x, zone.y)).collect::<Vec<_>>()}))
|
||||
Ok(
|
||||
json!({"zone_set":self.zone_set, "zones":self.zones.iter().map(|zone|
|
||||
(zone.width, zone.height, zone.x, zone.y)).collect::<Vec<_>>()}),
|
||||
)
|
||||
}
|
||||
|
||||
fn session(&self, id: u64) -> Result<&Session, String> {
|
||||
self.sessions.get(&id).ok_or("Unknown input capture session".into())
|
||||
self.sessions
|
||||
.get(&id)
|
||||
.ok_or("Unknown input capture session".into())
|
||||
}
|
||||
|
||||
pub(crate) fn set_barriers(&mut self, id: u64, zone_set: u32, barriers: Vec<Barrier>) -> Result<Vec<u32>, String> {
|
||||
pub(crate) fn set_barriers(
|
||||
&mut self,
|
||||
id: u64,
|
||||
zone_set: u32,
|
||||
barriers: Vec<Barrier>,
|
||||
) -> Result<Vec<u32>, String> {
|
||||
self.session(id)?;
|
||||
if barriers.len() > MAX_BARRIERS {
|
||||
return Err("Too many pointer barriers".into());
|
||||
@@ -229,7 +251,10 @@ impl InputCapture {
|
||||
}
|
||||
|
||||
pub(crate) fn enable(&mut self, id: u64) -> Result<(), String> {
|
||||
let session = self.sessions.get_mut(&id).ok_or("Unknown input capture session")?;
|
||||
let session = self
|
||||
.sessions
|
||||
.get_mut(&id)
|
||||
.ok_or("Unknown input capture session")?;
|
||||
if session.closed {
|
||||
return Err("Input capture session is closed".into());
|
||||
}
|
||||
@@ -255,7 +280,11 @@ impl InputCapture {
|
||||
if self.active() {
|
||||
return true;
|
||||
}
|
||||
if !allowed || ![from.0, from.1, to.0, to.1].iter().all(|value| value.is_finite()) {
|
||||
if !allowed
|
||||
|| ![from.0, from.1, to.0, to.1]
|
||||
.iter()
|
||||
.all(|value| value.is_finite())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let hit = self
|
||||
@@ -340,7 +369,10 @@ impl InputCapture {
|
||||
self.restore_focus = true;
|
||||
let session = self.sessions.get_mut(&id).unwrap();
|
||||
let activation = session.activation.take();
|
||||
self.enqueue(id, json!({"type":"deactivated", "activation_id":activation}));
|
||||
self.enqueue(
|
||||
id,
|
||||
json!({"type":"deactivated", "activation_id":activation}),
|
||||
);
|
||||
self.flush();
|
||||
}
|
||||
Ok(())
|
||||
@@ -365,10 +397,18 @@ impl InputCapture {
|
||||
}
|
||||
|
||||
pub(crate) fn valid_position(&self, point: (f64, f64)) -> bool {
|
||||
point.0.is_finite() && point.1.is_finite() && self.zones.iter().any(|zone| zone.contains(point))
|
||||
point.0.is_finite()
|
||||
&& point.1.is_finite()
|
||||
&& self.zones.iter().any(|zone| zone.contains(point))
|
||||
}
|
||||
|
||||
pub(crate) fn watch(&mut self, owner: u64, request: u64, id: u64, response: SyncSender<Response>) {
|
||||
pub(crate) fn watch(
|
||||
&mut self,
|
||||
owner: u64,
|
||||
request: u64,
|
||||
id: u64,
|
||||
response: SyncSender<Response>,
|
||||
) {
|
||||
if !self.sessions.contains_key(&id)
|
||||
|| self.waiters.len() >= MAX_SESSIONS
|
||||
|| self.waiters.values().any(|(_, token, _)| *token == id)
|
||||
@@ -479,10 +519,19 @@ impl crate::Ferese {
|
||||
&& !self.overview.is_presenting()
|
||||
&& !pointer.is_grabbed()
|
||||
&& !self.swipe.active()
|
||||
&& self.seat.get_keyboard().is_none_or(|keyboard| !keyboard.is_grabbed())
|
||||
&& self.seat.get_touch().is_none_or(|touch| !touch.is_grabbed())
|
||||
&& self
|
||||
.seat
|
||||
.get_keyboard()
|
||||
.is_none_or(|keyboard| !keyboard.is_grabbed())
|
||||
&& self
|
||||
.seat
|
||||
.get_touch()
|
||||
.is_none_or(|touch| !touch.is_grabbed())
|
||||
&& !self.seat.keyboard_shortcuts_inhibited();
|
||||
if !self.input_capture.motion((from.x, from.y), (to.x, to.y), allowed) {
|
||||
if !self
|
||||
.input_capture
|
||||
.motion((from.x, from.y), (to.x, to.y), allowed)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let delta = if absolute {
|
||||
@@ -553,7 +602,10 @@ mod tests {
|
||||
[3840, 0, 3840, 1079],
|
||||
[0, 0, 0, 1079],
|
||||
] {
|
||||
assert!(edge(&Barrier { id: 1, position }, &zones).is_some(), "{position:?}");
|
||||
assert!(
|
||||
edge(&Barrier { id: 1, position }, &zones).is_some(),
|
||||
"{position:?}"
|
||||
);
|
||||
}
|
||||
for position in [
|
||||
[1920, 0, 1920, 1079],
|
||||
@@ -562,7 +614,10 @@ mod tests {
|
||||
[50, 50, 50, 200],
|
||||
[0, 5, 0, 4],
|
||||
] {
|
||||
assert!(edge(&Barrier { id: 1, position }, &zones).is_none(), "{position:?}");
|
||||
assert!(
|
||||
edge(&Barrier { id: 1, position }, &zones).is_none(),
|
||||
"{position:?}"
|
||||
);
|
||||
}
|
||||
let barrier = Barrier {
|
||||
id: 1,
|
||||
@@ -651,7 +706,11 @@ mod tests {
|
||||
capture.enable(id).unwrap();
|
||||
assert!(capture.motion((3839.0, 50.0), (3900.0, 50.0), true));
|
||||
let activation = capture.sessions[&id].activation.unwrap();
|
||||
assert!(capture.release(id, Some(activation.wrapping_add(1))).is_err());
|
||||
assert!(
|
||||
capture
|
||||
.release(id, Some(activation.wrapping_add(1)))
|
||||
.is_err()
|
||||
);
|
||||
capture.release(id, Some(activation)).unwrap();
|
||||
assert!(capture.sessions[&id].enabled);
|
||||
capture.update_zones(vec![zones()[0]]);
|
||||
|
||||
+327
-123
@@ -18,12 +18,16 @@ mod transport;
|
||||
use ferese_layout::Direction;
|
||||
use serde_json::{Value, json};
|
||||
use smithay::output::Output;
|
||||
use smithay::reexports::calloop::{EventLoop, LoopHandle, LoopSignal, RegistrationToken, channel, timer};
|
||||
use smithay::reexports::calloop::{
|
||||
EventLoop, LoopHandle, LoopSignal, RegistrationToken, channel, timer,
|
||||
};
|
||||
use smithay::utils::Transform;
|
||||
|
||||
use crate::Ferese;
|
||||
use crate::config::OutputTransform;
|
||||
use crate::handlers::screenshot::{Action, Geometry, OutputLayout, PartOutcome, PartSender, parse_geometry, plan};
|
||||
use crate::handlers::screenshot::{
|
||||
Action, Geometry, OutputLayout, PartOutcome, PartSender, parse_geometry, plan,
|
||||
};
|
||||
use crate::handlers::screenshot_worker::{Encoded, Job, Worker};
|
||||
use crate::handlers::{screencopy, screenshot_worker};
|
||||
|
||||
@@ -107,7 +111,10 @@ impl Endpoint {
|
||||
|
||||
fn desktop_at(path: PathBuf) -> io::Result<Self> {
|
||||
prepare_parent(&path)?;
|
||||
Ok(Self { path, instance: None })
|
||||
Ok(Self {
|
||||
path,
|
||||
instance: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn instance() -> io::Result<Self> {
|
||||
@@ -115,7 +122,9 @@ impl Endpoint {
|
||||
}
|
||||
|
||||
fn instance_at(default: PathBuf) -> io::Result<Self> {
|
||||
let root = default.parent().expect("runtime control socket has a parent");
|
||||
let root = default
|
||||
.parent()
|
||||
.expect("runtime control socket has a parent");
|
||||
prepare_directory(root)?;
|
||||
let instances = root.join("instances");
|
||||
prepare_directory(&instances)?;
|
||||
@@ -133,7 +142,9 @@ impl Endpoint {
|
||||
impl Drop for IpcSocketGuard {
|
||||
fn drop(&mut self) {
|
||||
if self.path.symlink_metadata().is_ok_and(|metadata| {
|
||||
metadata.file_type().is_socket() && metadata.dev() == self.device && metadata.ino() == self.inode
|
||||
metadata.file_type().is_socket()
|
||||
&& metadata.dev() == self.device
|
||||
&& metadata.ino() == self.inode
|
||||
}) {
|
||||
let _ = fs::remove_file(&self.path);
|
||||
}
|
||||
@@ -158,7 +169,9 @@ pub(crate) fn init(
|
||||
|
||||
let (sender, receiver): (channel::SyncSender<IpcEvent>, channel::Channel<IpcEvent>) =
|
||||
channel::sync_channel(REQUEST_QUEUE_CAPACITY);
|
||||
event_loop.handle().insert_source(receiver, |event, _, state| {
|
||||
event_loop
|
||||
.handle()
|
||||
.insert_source(receiver, |event, _, state| {
|
||||
if let channel::Event::Msg(event) = event {
|
||||
let call = match event {
|
||||
IpcEvent::Call(call) => *call,
|
||||
@@ -174,7 +187,9 @@ pub(crate) fn init(
|
||||
})
|
||||
.and_then(|output| output["name"].as_str())
|
||||
.map(str::to_owned);
|
||||
state.activities.detect_process(owner, *matched, output, &fd);
|
||||
state
|
||||
.activities
|
||||
.detect_process(owner, *matched, output, &fd);
|
||||
return;
|
||||
}
|
||||
IpcEvent::ActivityOwnerExited(owner) => {
|
||||
@@ -209,7 +224,8 @@ pub(crate) fn init(
|
||||
if state.session_lock.active() {
|
||||
return Err("Session locked".into());
|
||||
}
|
||||
let (owner, publish, matched) = call.activity.ok_or("Unverified activity owner")?;
|
||||
let (owner, publish, matched) =
|
||||
call.activity.ok_or("Unverified activity owner")?;
|
||||
let output = state
|
||||
.outputs_json()
|
||||
.as_array()
|
||||
@@ -240,7 +256,8 @@ pub(crate) fn init(
|
||||
return Err("Session locked".into());
|
||||
}
|
||||
let request: ferese_ipc::activities::PomodoroRequest =
|
||||
serde_json::from_value(call.request.args).map_err(|error| error.to_string())?;
|
||||
serde_json::from_value(call.request.args)
|
||||
.map_err(|error| error.to_string())?;
|
||||
let output = state
|
||||
.outputs_json()
|
||||
.as_array()
|
||||
@@ -267,9 +284,11 @@ pub(crate) fn init(
|
||||
"IPC unavailable while session is locked",
|
||||
));
|
||||
} else if let Err(error) = validate_request(&call.request) {
|
||||
let _ = call
|
||||
.response
|
||||
.try_send(Response::error(call.request.id, error.code, error.message));
|
||||
let _ = call.response.try_send(Response::error(
|
||||
call.request.id,
|
||||
error.code,
|
||||
error.message,
|
||||
));
|
||||
} else {
|
||||
state.activities.get(call.request.id, call.response);
|
||||
}
|
||||
@@ -281,9 +300,11 @@ pub(crate) fn init(
|
||||
"IPC unavailable while session is locked",
|
||||
));
|
||||
} else if let Err(error) = validate_request(&call.request) {
|
||||
let _ = call
|
||||
.response
|
||||
.try_send(Response::error(call.request.id, error.code, error.message));
|
||||
let _ = call.response.try_send(Response::error(
|
||||
call.request.id,
|
||||
error.code,
|
||||
error.message,
|
||||
));
|
||||
} else if let Some(since) = call.request.args["since"].as_u64() {
|
||||
state
|
||||
.activities
|
||||
@@ -297,7 +318,9 @@ pub(crate) fn init(
|
||||
}
|
||||
} else if call.request.command == "event-stream" {
|
||||
let result = validate_request(&call.request).and_then(|()| {
|
||||
if call.request.args["version"].as_u64() != Some(u64::from(ferese_ipc::events::VERSION)) {
|
||||
if call.request.args["version"].as_u64()
|
||||
!= Some(u64::from(ferese_ipc::events::VERSION))
|
||||
{
|
||||
return Err(CommandError::new(
|
||||
"unsupported_event_version",
|
||||
"Unsupported event schema version",
|
||||
@@ -309,11 +332,15 @@ pub(crate) fn init(
|
||||
.map_err(|error| CommandError::new("subscriber_limit", error))
|
||||
});
|
||||
let response = match result {
|
||||
Ok(()) => Response::success(call.request.id, json!({"version": ferese_ipc::events::VERSION})),
|
||||
Ok(()) => Response::success(
|
||||
call.request.id,
|
||||
json!({"version": ferese_ipc::events::VERSION}),
|
||||
),
|
||||
Err(error) => Response::error(call.request.id, error.code, error.message),
|
||||
};
|
||||
let _ = call.response.try_send(response);
|
||||
} else if call.request.command.starts_with("input-capture-") && !call.native_portal {
|
||||
} else if call.request.command.starts_with("input-capture-") && !call.native_portal
|
||||
{
|
||||
let _ = call.response.try_send(Response::error(
|
||||
call.request.id,
|
||||
"access_denied",
|
||||
@@ -321,9 +348,11 @@ pub(crate) fn init(
|
||||
));
|
||||
} else if call.request.command == "input-capture-watch" {
|
||||
if let Err(error) = validate_request(&call.request) {
|
||||
let _ = call
|
||||
.response
|
||||
.try_send(Response::error(call.request.id, error.code, error.message));
|
||||
let _ = call.response.try_send(Response::error(
|
||||
call.request.id,
|
||||
error.code,
|
||||
error.message,
|
||||
));
|
||||
} else if let Some(id) = call.request.args["session"].as_u64() {
|
||||
state
|
||||
.input_capture
|
||||
@@ -335,11 +364,16 @@ pub(crate) fn init(
|
||||
"Missing capture session",
|
||||
));
|
||||
}
|
||||
} else if matches!(call.request.command.as_str(), "media-watch" | "media-action") {
|
||||
} else if matches!(
|
||||
call.request.command.as_str(),
|
||||
"media-watch" | "media-action"
|
||||
) {
|
||||
if let Err(error) = validate_request(&call.request) {
|
||||
let _ = call
|
||||
.response
|
||||
.try_send(Response::error(call.request.id, error.code, error.message));
|
||||
let _ = call.response.try_send(Response::error(
|
||||
call.request.id,
|
||||
error.code,
|
||||
error.message,
|
||||
));
|
||||
} else if state.session_lock.active() {
|
||||
let _ = call.response.try_send(Response::error(
|
||||
call.request.id,
|
||||
@@ -348,9 +382,12 @@ pub(crate) fn init(
|
||||
));
|
||||
} else if call.request.command == "media-watch" {
|
||||
if let Some(since) = call.request.args["since"].as_u64() {
|
||||
state
|
||||
.media_engine
|
||||
.watch(call.owner, call.request.id, since, call.response);
|
||||
state.media_engine.watch(
|
||||
call.owner,
|
||||
call.request.id,
|
||||
since,
|
||||
call.response,
|
||||
);
|
||||
} else {
|
||||
let _ = call.response.try_send(Response::error(
|
||||
call.request.id,
|
||||
@@ -358,19 +395,23 @@ pub(crate) fn init(
|
||||
"Missing media revision",
|
||||
));
|
||||
}
|
||||
} else if let Err(error) = state
|
||||
.media_engine
|
||||
.action(call.request.args, Some((call.request.id, call.response.clone())))
|
||||
{
|
||||
let _ = call
|
||||
.response
|
||||
.try_send(Response::error(call.request.id, "media_unavailable", error));
|
||||
} else if let Err(error) = state.media_engine.action(
|
||||
call.request.args,
|
||||
Some((call.request.id, call.response.clone())),
|
||||
) {
|
||||
let _ = call.response.try_send(Response::error(
|
||||
call.request.id,
|
||||
"media_unavailable",
|
||||
error,
|
||||
));
|
||||
}
|
||||
} else if call.request.command == "theme-watch" {
|
||||
if let Err(error) = validate_request(&call.request) {
|
||||
let _ = call
|
||||
.response
|
||||
.try_send(Response::error(call.request.id, error.code, error.message));
|
||||
let _ = call.response.try_send(Response::error(
|
||||
call.request.id,
|
||||
error.code,
|
||||
error.message,
|
||||
));
|
||||
} else if let Some(since) = call.request.args["since"].as_u64() {
|
||||
state
|
||||
.theme_engine
|
||||
@@ -384,13 +425,18 @@ pub(crate) fn init(
|
||||
}
|
||||
} else if call.request.command == "session-watch" {
|
||||
if let Err(error) = validate_request(&call.request) {
|
||||
let _ = call
|
||||
.response
|
||||
.send(Response::error(call.request.id, error.code, error.message));
|
||||
let _ = call.response.send(Response::error(
|
||||
call.request.id,
|
||||
error.code,
|
||||
error.message,
|
||||
));
|
||||
} else if let Some(since) = call.request.args["since"].as_u64() {
|
||||
state
|
||||
.portal_session
|
||||
.watch(call.owner, call.request.id, since, call.response);
|
||||
state.portal_session.watch(
|
||||
call.owner,
|
||||
call.request.id,
|
||||
since,
|
||||
call.response,
|
||||
);
|
||||
} else {
|
||||
let _ = call.response.send(Response::error(
|
||||
call.request.id,
|
||||
@@ -400,16 +446,20 @@ pub(crate) fn init(
|
||||
}
|
||||
} else if call.request.command == "reload-config" {
|
||||
if let Err(error) = validate_request(&call.request) {
|
||||
let _ = call
|
||||
.response
|
||||
.try_send(Response::error(call.request.id, error.code, error.message));
|
||||
let _ = call.response.try_send(Response::error(
|
||||
call.request.id,
|
||||
error.code,
|
||||
error.message,
|
||||
));
|
||||
} else if state.session_lock.active() {
|
||||
let _ = call.response.try_send(Response::error(
|
||||
call.request.id,
|
||||
"session_locked",
|
||||
"IPC unavailable while session is locked",
|
||||
));
|
||||
} else if let Err(error) = state.queue_config_reload(false, Some((call.request.id, call.response))) {
|
||||
} else if let Err(error) =
|
||||
state.queue_config_reload(false, Some((call.request.id, call.response)))
|
||||
{
|
||||
tracing::warn!(%error, "cannot queue configuration reload");
|
||||
}
|
||||
} else if call.request.command == "screenshot-window" {
|
||||
@@ -428,14 +478,18 @@ pub(crate) fn init(
|
||||
// Screenshot readbacks publish parts here, so the loop wakes as soon as a
|
||||
// readback lands, whichever backend produced it.
|
||||
let (parts, part_events) = channel::channel::<PartOutcome>();
|
||||
event_loop.handle().insert_source(part_events, |event, _, state| {
|
||||
event_loop
|
||||
.handle()
|
||||
.insert_source(part_events, |event, _, state| {
|
||||
if let channel::Event::Msg(outcome) = event {
|
||||
state.on_screenshot_part(outcome);
|
||||
}
|
||||
})?;
|
||||
|
||||
let (encoded, encoded_events) = channel::sync_channel::<Encoded>(RESULT_QUEUE_CAPACITY);
|
||||
event_loop.handle().insert_source(encoded_events, |event, _, state| {
|
||||
event_loop
|
||||
.handle()
|
||||
.insert_source(encoded_events, |event, _, state| {
|
||||
if let channel::Event::Msg(result) = event {
|
||||
state.on_screenshot_encoded(result);
|
||||
}
|
||||
@@ -589,7 +643,10 @@ fn accept_connections(
|
||||
}
|
||||
|
||||
let Some(permit) = try_acquire_connection(&active_connections) else {
|
||||
tracing::warn!(limit = MAX_CONNECTIONS, "rejected IPC connection at worker limit");
|
||||
tracing::warn!(
|
||||
limit = MAX_CONNECTIONS,
|
||||
"rejected IPC connection at worker limit"
|
||||
);
|
||||
continue;
|
||||
};
|
||||
let sender = sender.clone();
|
||||
@@ -601,7 +658,13 @@ fn accept_connections(
|
||||
.name("ferese-ipc-client".to_owned())
|
||||
.spawn(move || {
|
||||
let _permit = permit;
|
||||
serve_connection_with_activities(stream, sender, signal, owner, Some((monitor, rules)));
|
||||
serve_connection_with_activities(
|
||||
stream,
|
||||
sender,
|
||||
signal,
|
||||
owner,
|
||||
Some((monitor, rules)),
|
||||
);
|
||||
})
|
||||
{
|
||||
tracing::warn!(%error, "could not start IPC connection worker");
|
||||
@@ -621,11 +684,18 @@ fn try_acquire_connection(active: &Arc<AtomicUsize>) -> Option<ConnectionPermit>
|
||||
}
|
||||
}
|
||||
|
||||
Some(ConnectionPermit { active: active.clone() })
|
||||
Some(ConnectionPermit {
|
||||
active: active.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn serve_connection(stream: UnixStream, sender: channel::SyncSender<IpcEvent>, signal: LoopSignal, owner: u64) {
|
||||
fn serve_connection(
|
||||
stream: UnixStream,
|
||||
sender: channel::SyncSender<IpcEvent>,
|
||||
signal: LoopSignal,
|
||||
owner: u64,
|
||||
) {
|
||||
serve_connection_with_activities(stream, sender, signal, owner, None)
|
||||
}
|
||||
fn serve_connection_with_activities(
|
||||
@@ -648,7 +718,9 @@ fn serve_connection_with_activities(
|
||||
Err(ferese_ipc::FrameError::Io(error))
|
||||
if matches!(
|
||||
error.kind(),
|
||||
io::ErrorKind::UnexpectedEof | io::ErrorKind::ConnectionReset | io::ErrorKind::BrokenPipe
|
||||
io::ErrorKind::UnexpectedEof
|
||||
| io::ErrorKind::ConnectionReset
|
||||
| io::ErrorKind::BrokenPipe
|
||||
) =>
|
||||
{
|
||||
return;
|
||||
@@ -675,7 +747,8 @@ fn serve_connection_with_activities(
|
||||
let verified = (|| -> Result<_, String> {
|
||||
validate_request(&request).map_err(|error| error.message)?;
|
||||
let wire: ferese_ipc::activities::Request =
|
||||
serde_json::from_value(std::mem::take(&mut request.args)).map_err(|error| error.to_string())?;
|
||||
serde_json::from_value(std::mem::take(&mut request.args))
|
||||
.map_err(|error| error.to_string())?;
|
||||
if wire.version != ferese_ipc::activities::VERSION {
|
||||
return Err("Unsupported activity schema".into());
|
||||
}
|
||||
@@ -688,10 +761,14 @@ fn serve_connection_with_activities(
|
||||
.map_err(|error| error.to_string())?;
|
||||
let mut publish = wire.publish;
|
||||
let is_run = matches!(publish, ferese_ipc::activities::Publish::Run { .. });
|
||||
let matched = if let ferese_ipc::activities::Publish::Hook { command, cwd, .. }
|
||||
let matched =
|
||||
if let ferese_ipc::activities::Publish::Hook { command, cwd, .. }
|
||||
| ferese_ipc::activities::Publish::Run { command, cwd, .. } = &mut publish
|
||||
{
|
||||
let rules = rules.read().map_err(|_| "Activity rules unavailable")?.clone();
|
||||
let rules = rules
|
||||
.read()
|
||||
.map_err(|_| "Activity rules unavailable")?
|
||||
.clone();
|
||||
let matched = if rules.enabled || is_run {
|
||||
rules.matcher.matches(command, cwd, &parents)
|
||||
} else {
|
||||
@@ -705,8 +782,10 @@ fn serve_connection_with_activities(
|
||||
};
|
||||
if matches!(
|
||||
publish,
|
||||
ferese_ipc::activities::Publish::Start { .. } | ferese_ipc::activities::Publish::Run { .. }
|
||||
) || (matches!(publish, ferese_ipc::activities::Publish::Hook { .. }) && matched.is_some())
|
||||
ferese_ipc::activities::Publish::Start { .. }
|
||||
| ferese_ipc::activities::Publish::Run { .. }
|
||||
) || (matches!(publish, ferese_ipc::activities::Publish::Hook { .. })
|
||||
&& matched.is_some())
|
||||
{
|
||||
identity.capture_ancestors();
|
||||
}
|
||||
@@ -715,7 +794,10 @@ fn serve_connection_with_activities(
|
||||
match verified {
|
||||
Ok(verified) => Some(verified),
|
||||
Err(error) => {
|
||||
if transport::write_response(&stream, &Response::error(request.id, "owner_rejected", error))
|
||||
if transport::write_response(
|
||||
&stream,
|
||||
&Response::error(request.id, "owner_rejected", error),
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
return;
|
||||
@@ -738,7 +820,8 @@ fn serve_connection_with_activities(
|
||||
| "input-capture-register"
|
||||
| "begin-session-end"
|
||||
);
|
||||
let screenshot_requested = matches!(request.command.as_str(), "screenshot" | "screenshot-window");
|
||||
let screenshot_requested =
|
||||
matches!(request.command.as_str(), "screenshot" | "screenshot-window");
|
||||
let call = IpcEvent::Call(Box::new(IpcCall {
|
||||
native_portal,
|
||||
activity,
|
||||
@@ -812,7 +895,10 @@ fn serve_connection_with_activities(
|
||||
}
|
||||
|
||||
// Socket I/O stays on the bounded connection worker, never the compositor loop.
|
||||
fn relay_events(stream: &mut UnixStream, events: std::sync::mpsc::Receiver<Arc<ferese_ipc::events::Event>>) {
|
||||
fn relay_events(
|
||||
stream: &mut UnixStream,
|
||||
events: std::sync::mpsc::Receiver<Arc<ferese_ipc::events::Event>>,
|
||||
) {
|
||||
loop {
|
||||
match events.recv_timeout(Duration::from_millis(250)) {
|
||||
Ok(event) => {
|
||||
@@ -855,7 +941,10 @@ fn u32_arg(args: &Value, key: &str) -> Result<u32, CommandError> {
|
||||
|
||||
fn discard_undelivered_screenshot(response: &Response) {
|
||||
if response.error.is_none()
|
||||
&& let Some(path) = response.result.as_ref().and_then(|result| result["path"].as_str())
|
||||
&& let Some(path) = response
|
||||
.result
|
||||
.as_ref()
|
||||
.and_then(|result| result["path"].as_str())
|
||||
&& let Err(error) = fs::remove_file(path)
|
||||
&& error.kind() != io::ErrorKind::NotFound
|
||||
{
|
||||
@@ -882,7 +971,11 @@ impl Ferese {
|
||||
| "cancel-session-end"
|
||||
)
|
||||
{
|
||||
return Response::error(request.id, "session_locked", "IPC unavailable while session is locked");
|
||||
return Response::error(
|
||||
request.id,
|
||||
"session_locked",
|
||||
"IPC unavailable while session is locked",
|
||||
);
|
||||
}
|
||||
if let Err(error) = validate_request(&request) {
|
||||
return Response::error(request.id, error.code, error.message);
|
||||
@@ -894,7 +987,12 @@ impl Ferese {
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatch_ipc_command(&mut self, owner: u64, command: &str, args: &Value) -> Result<Value, CommandError> {
|
||||
fn dispatch_ipc_command(
|
||||
&mut self,
|
||||
owner: u64,
|
||||
command: &str,
|
||||
args: &Value,
|
||||
) -> Result<Value, CommandError> {
|
||||
match command {
|
||||
"theme-get" | "theme-status" => return Ok(self.theme_engine.value()),
|
||||
"theme-set-mode" => {
|
||||
@@ -905,7 +1003,9 @@ impl Ferese {
|
||||
.map_err(|error| CommandError::new("invalid_config", error));
|
||||
}
|
||||
"night-light" => {
|
||||
return Ok(json!(crate::backends::direct::gamma::night_light_status(self)));
|
||||
return Ok(json!(crate::backends::direct::gamma::night_light_status(
|
||||
self
|
||||
)));
|
||||
}
|
||||
"portal-inhibit" => {
|
||||
let inhibition = serde_json::from_value(args.clone())
|
||||
@@ -923,7 +1023,8 @@ impl Ferese {
|
||||
return Ok(crate::xwayland::status_snapshot(self).to_value());
|
||||
}
|
||||
ferese_ipc::xwayland::RETRY_COMMAND => {
|
||||
crate::xwayland::retry(self).map_err(|error| CommandError::new("x11_unavailable", error))?;
|
||||
crate::xwayland::retry(self)
|
||||
.map_err(|error| CommandError::new("x11_unavailable", error))?;
|
||||
|
||||
return Ok(crate::xwayland::status_snapshot(self).to_value());
|
||||
}
|
||||
@@ -955,9 +1056,9 @@ impl Ferese {
|
||||
.validate_end(
|
||||
u32_arg(args, "token")?,
|
||||
u32_arg(args, "inhibitor-revision")?,
|
||||
args["force"]
|
||||
.as_bool()
|
||||
.ok_or_else(|| CommandError::new("invalid_argument", "Missing force confirmation"))?,
|
||||
args["force"].as_bool().ok_or_else(|| {
|
||||
CommandError::new("invalid_argument", "Missing force confirmation")
|
||||
})?,
|
||||
)
|
||||
.map_err(|error| CommandError::new("inhibited", error))?;
|
||||
return Ok(json!({}));
|
||||
@@ -967,17 +1068,17 @@ impl Ferese {
|
||||
.commit_end(
|
||||
u32_arg(args, "token")?,
|
||||
u32_arg(args, "inhibitor-revision")?,
|
||||
args["force"]
|
||||
.as_bool()
|
||||
.ok_or_else(|| CommandError::new("invalid_argument", "Missing force confirmation"))?,
|
||||
args["force"].as_bool().ok_or_else(|| {
|
||||
CommandError::new("invalid_argument", "Missing force confirmation")
|
||||
})?,
|
||||
)
|
||||
.map_err(|error| CommandError::new("inhibited", error))?;
|
||||
return Ok(json!({}));
|
||||
}
|
||||
"logind-session-ending" => {
|
||||
let ending = args["ending"]
|
||||
.as_bool()
|
||||
.ok_or_else(|| CommandError::new("invalid_argument", "Missing shutdown state"))?;
|
||||
let ending = args["ending"].as_bool().ok_or_else(|| {
|
||||
CommandError::new("invalid_argument", "Missing shutdown state")
|
||||
})?;
|
||||
self.portal_session.set_phase(if ending { 3 } else { 1 });
|
||||
return Ok(json!({}));
|
||||
}
|
||||
@@ -990,7 +1091,9 @@ impl Ferese {
|
||||
return Ok(json!({}));
|
||||
}
|
||||
"portal-shortcuts-poll" => {
|
||||
return Ok(self.portal_shortcuts.poll(owner, self.session_lock.active()));
|
||||
return Ok(self
|
||||
.portal_shortcuts
|
||||
.poll(owner, self.session_lock.active()));
|
||||
}
|
||||
"input-capture-register" => {
|
||||
if self.session_lock.active() {
|
||||
@@ -1006,7 +1109,8 @@ impl Ferese {
|
||||
let xkb = state.xkb().lock().unwrap();
|
||||
// SAFETY: the ref-counted keymap does not escape the locked Xkb;
|
||||
// only its owned string is retained for the EIS keyboard.
|
||||
unsafe { xkb.keymap() }.get_as_string(smithay::input::keyboard::xkb::KEYMAP_FORMAT_TEXT_V1)
|
||||
unsafe { xkb.keymap() }
|
||||
.get_as_string(smithay::input::keyboard::xkb::KEYMAP_FORMAT_TEXT_V1)
|
||||
});
|
||||
return Ok(json!({"session":id, "keymap":keymap}));
|
||||
}
|
||||
@@ -1015,9 +1119,9 @@ impl Ferese {
|
||||
return self
|
||||
.input_capture
|
||||
.zones(
|
||||
args["session"]
|
||||
.as_u64()
|
||||
.ok_or_else(|| CommandError::new("invalid_capture", "Missing session"))?,
|
||||
args["session"].as_u64().ok_or_else(|| {
|
||||
CommandError::new("invalid_capture", "Missing session")
|
||||
})?,
|
||||
)
|
||||
.map_err(|e| CommandError::new("invalid_capture", e));
|
||||
}
|
||||
@@ -1074,8 +1178,12 @@ impl Ferese {
|
||||
let result = if command == "input-capture-disable" {
|
||||
self.input_capture.disable(id)
|
||||
} else {
|
||||
self.input_capture
|
||||
.release(id, args["activation_id"].as_u64().and_then(|id| u32::try_from(id).ok()))
|
||||
self.input_capture.release(
|
||||
id,
|
||||
args["activation_id"]
|
||||
.as_u64()
|
||||
.and_then(|id| u32::try_from(id).ok()),
|
||||
)
|
||||
};
|
||||
result.map_err(|e| CommandError::new("invalid_capture", e))?;
|
||||
if active && !self.input_capture.active() {
|
||||
@@ -1166,7 +1274,9 @@ impl Ferese {
|
||||
.get("pid")
|
||||
.and_then(Value::as_u64)
|
||||
.and_then(|pid| u32::try_from(pid).ok())
|
||||
.ok_or_else(|| CommandError::new("invalid_argument", "pid must be an unsigned process ID"))?;
|
||||
.ok_or_else(|| {
|
||||
CommandError::new("invalid_argument", "pid must be an unsigned process ID")
|
||||
})?;
|
||||
return Ok(json!(self.has_client_surfaces(pid)));
|
||||
}
|
||||
"get-workspaces" => return Ok(self.workspaces_json()),
|
||||
@@ -1185,29 +1295,38 @@ impl Ferese {
|
||||
}));
|
||||
}
|
||||
"output-confirm" | "output-revert" => {
|
||||
crate::backends::direct::confirm_output_configuration(self, command == "output-confirm")
|
||||
crate::backends::direct::confirm_output_configuration(
|
||||
self,
|
||||
command == "output-confirm",
|
||||
)
|
||||
.map_err(|error| CommandError::new("output_configuration_failed", error))?;
|
||||
return Ok(self.outputs_json());
|
||||
}
|
||||
"output-profile" => {
|
||||
let name = args["name"]
|
||||
.as_str()
|
||||
.ok_or_else(|| CommandError::new("invalid_arguments", "output-profile requires a name or auto"))?;
|
||||
let name = args["name"].as_str().ok_or_else(|| {
|
||||
CommandError::new(
|
||||
"invalid_arguments",
|
||||
"output-profile requires a name or auto",
|
||||
)
|
||||
})?;
|
||||
crate::backends::direct::set_output_profile(self, name)
|
||||
.map_err(|error| CommandError::new("output_configuration_failed", error))?;
|
||||
return Ok(self.outputs_json());
|
||||
}
|
||||
"output-layout" => {
|
||||
let layout = serde_json::from_value::<crate::config::OutputLayout>(args["layout"].clone())
|
||||
.map_err(|_| CommandError::new("invalid_arguments", "unknown display layout"))?;
|
||||
let layout =
|
||||
serde_json::from_value::<crate::config::OutputLayout>(args["layout"].clone())
|
||||
.map_err(|_| {
|
||||
CommandError::new("invalid_arguments", "unknown display layout")
|
||||
})?;
|
||||
crate::backends::direct::set_output_layout(self, layout)
|
||||
.map_err(|error| CommandError::new("output_configuration_failed", error))?;
|
||||
return Ok(self.outputs_json());
|
||||
}
|
||||
"output-internal" => {
|
||||
let enabled = args["enabled"]
|
||||
.as_bool()
|
||||
.ok_or_else(|| CommandError::new("invalid_arguments", "output-internal requires on or off"))?;
|
||||
let enabled = args["enabled"].as_bool().ok_or_else(|| {
|
||||
CommandError::new("invalid_arguments", "output-internal requires on or off")
|
||||
})?;
|
||||
crate::backends::direct::set_internal_output(self, enabled)
|
||||
.map_err(|error| CommandError::new("output_configuration_failed", error))?;
|
||||
return Ok(self.outputs_json());
|
||||
@@ -1225,7 +1344,12 @@ impl Ferese {
|
||||
|
||||
// Screenshot replies are deferred: this either answers the caller now, or
|
||||
// hands the reply to the coordinator, which answers exactly once.
|
||||
pub(crate) fn start_screenshot(&mut self, owner: u64, request: Request, response: SyncSender<Response>) {
|
||||
pub(crate) fn start_screenshot(
|
||||
&mut self,
|
||||
owner: u64,
|
||||
request: Request,
|
||||
response: SyncSender<Response>,
|
||||
) {
|
||||
// A macro rather than a closure: the message may be a borrowed str or an
|
||||
// owned String, and Response::error already accepts either.
|
||||
macro_rules! reject {
|
||||
@@ -1292,7 +1416,10 @@ impl Ferese {
|
||||
return;
|
||||
}
|
||||
};
|
||||
let specs = planned.iter().map(|part| part.spec(&layouts[part.index])).collect();
|
||||
let specs = planned
|
||||
.iter()
|
||||
.map(|part| part.spec(&layouts[part.index]))
|
||||
.collect();
|
||||
|
||||
// admit stores the reply only on success, so a clone survives the
|
||||
// rejection path and every caller is answered exactly once.
|
||||
@@ -1300,7 +1427,8 @@ impl Ferese {
|
||||
let id = match self.screenshot.admit(owner, request.id, response, specs) {
|
||||
Ok(id) => id,
|
||||
Err(message) => {
|
||||
let _ = fallback.try_send(Response::error(request.id, "screenshot_rejected", message));
|
||||
let _ =
|
||||
fallback.try_send(Response::error(request.id, "screenshot_rejected", message));
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -1328,9 +1456,18 @@ impl Ferese {
|
||||
crate::backends::direct::render_on(self, &outputs);
|
||||
}
|
||||
|
||||
fn start_window_screenshot(&mut self, owner: u64, request: Request, response: SyncSender<Response>) {
|
||||
fn start_window_screenshot(
|
||||
&mut self,
|
||||
owner: u64,
|
||||
request: Request,
|
||||
response: SyncSender<Response>,
|
||||
) {
|
||||
let reject = |message: String| {
|
||||
let _ = response.try_send(Response::error(request.id, "window_capture_failed", message));
|
||||
let _ = response.try_send(Response::error(
|
||||
request.id,
|
||||
"window_capture_failed",
|
||||
message,
|
||||
));
|
||||
};
|
||||
if self.session_lock.active() || !screencopy::capture_allowed() {
|
||||
reject("Screen capture is unavailable".into());
|
||||
@@ -1386,16 +1523,25 @@ impl Ferese {
|
||||
buffer_height: size.h,
|
||||
};
|
||||
let fallback = response.clone();
|
||||
let capture = match self.screenshot.admit(owner, request.id, response, vec![spec]) {
|
||||
let capture = match self
|
||||
.screenshot
|
||||
.admit(owner, request.id, response, vec![spec])
|
||||
{
|
||||
Ok(capture) => capture,
|
||||
Err(error) => {
|
||||
let _ = fallback.try_send(Response::error(request.id, "screenshot_rejected", error));
|
||||
let _ =
|
||||
fallback.try_send(Response::error(request.id, "screenshot_rejected", error));
|
||||
return;
|
||||
}
|
||||
};
|
||||
let result = if let Some(backend) = &self.nested_backend {
|
||||
match backend.try_borrow_mut() {
|
||||
Ok(mut backend) => crate::render::capture_window_buffer(backend.renderer(), &window, geometry, scale),
|
||||
Ok(mut backend) => crate::render::capture_window_buffer(
|
||||
backend.renderer(),
|
||||
&window,
|
||||
geometry,
|
||||
scale,
|
||||
),
|
||||
Err(_) => Err("Window renderer is busy".into()),
|
||||
}
|
||||
} else if let Some(backend) = &mut self.direct_backend {
|
||||
@@ -1447,7 +1593,8 @@ impl Ferese {
|
||||
.is_ok()
|
||||
});
|
||||
if !submitted {
|
||||
self.screenshot.reject(request, "Screenshot encoding queue is full");
|
||||
self.screenshot
|
||||
.reject(request, "Screenshot encoding queue is full");
|
||||
}
|
||||
}
|
||||
Action::Delivered(path) => {
|
||||
@@ -1500,7 +1647,11 @@ impl Ferese {
|
||||
});
|
||||
ferese_ipc::events::Snapshot {
|
||||
outputs,
|
||||
workspaces: if locked { json!([]) } else { self.workspaces_json() },
|
||||
workspaces: if locked {
|
||||
json!([])
|
||||
} else {
|
||||
self.workspaces_json()
|
||||
},
|
||||
windows,
|
||||
focus: if locked {
|
||||
Value::Null
|
||||
@@ -1528,7 +1679,8 @@ impl Ferese {
|
||||
|
||||
fn has_client_surfaces(&self, pid: u32) -> bool {
|
||||
use smithay::reexports::wayland_server::Resource;
|
||||
let belongs_to = |surface: &smithay::reexports::wayland_server::protocol::wl_surface::WlSurface| {
|
||||
let belongs_to =
|
||||
|surface: &smithay::reexports::wayland_server::protocol::wl_surface::WlSurface| {
|
||||
surface
|
||||
.client()
|
||||
.and_then(|client| client.get_credentials(&self.display_handle).ok())
|
||||
@@ -1589,7 +1741,10 @@ impl Ferese {
|
||||
.workspace_views(&self.workspaces)
|
||||
.into_iter()
|
||||
.map(|view| {
|
||||
let workspace = self.workspaces.workspace(view.id).expect("workspace view exists");
|
||||
let workspace = self
|
||||
.workspaces
|
||||
.workspace(view.id)
|
||||
.expect("workspace view exists");
|
||||
let mode = match workspace.layout.mode() {
|
||||
LayoutMode::Scrolling => "scrolling",
|
||||
LayoutMode::Tree => "tree",
|
||||
@@ -1686,7 +1841,9 @@ impl Ferese {
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
outputs.sort_by(|left, right| left["connector"].as_str().cmp(&right["connector"].as_str()));
|
||||
outputs.sort_by(|left, right| {
|
||||
left["connector"].as_str().cmp(&right["connector"].as_str())
|
||||
});
|
||||
return Value::Array(outputs);
|
||||
}
|
||||
|
||||
@@ -1742,7 +1899,9 @@ fn geometry_arg(args: &Value) -> Result<Geometry, String> {
|
||||
match args.get("geometry") {
|
||||
None | Some(Value::Null) => Ok(Geometry::All),
|
||||
Some(value) => {
|
||||
let text = value.as_str().ok_or("geometry must be a string like \"x,y WxH\"")?;
|
||||
let text = value
|
||||
.as_str()
|
||||
.ok_or("geometry must be a string like \"x,y WxH\"")?;
|
||||
parse_geometry(text)
|
||||
}
|
||||
}
|
||||
@@ -1756,7 +1915,10 @@ fn validate_request(request: &Request) -> Result<(), CommandError> {
|
||||
));
|
||||
}
|
||||
if request.kind != "command" {
|
||||
return Err(CommandError::new("invalid_type", "expected type \"command\""));
|
||||
return Err(CommandError::new(
|
||||
"invalid_type",
|
||||
"expected type \"command\"",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -1821,11 +1983,18 @@ fn workspace_arg(args: &Value) -> Result<u32, CommandError> {
|
||||
.and_then(Value::as_u64)
|
||||
.and_then(|index| u32::try_from(index).ok())
|
||||
.filter(|index| *index > 0)
|
||||
.ok_or_else(|| CommandError::new("invalid_argument", "index must be a positive 32-bit integer"))
|
||||
.ok_or_else(|| {
|
||||
CommandError::new(
|
||||
"invalid_argument",
|
||||
"index must be a positive 32-bit integer",
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn prepare_parent(socket: &Path) -> Result<(), io::Error> {
|
||||
let parent = socket.parent().expect("the Ferese control socket always has a parent");
|
||||
let parent = socket
|
||||
.parent()
|
||||
.expect("the Ferese control socket always has a parent");
|
||||
prepare_directory(parent)
|
||||
}
|
||||
|
||||
@@ -1878,7 +2047,11 @@ fn bind_listener(path: &Path) -> Result<UnixListener, io::Error> {
|
||||
}
|
||||
|
||||
fn peer_uid(stream: &UnixStream) -> Result<u32, io::Error> {
|
||||
let mut credentials = libc::ucred { pid: 0, uid: 0, gid: 0 };
|
||||
let mut credentials = libc::ucred {
|
||||
pid: 0,
|
||||
uid: 0,
|
||||
gid: 0,
|
||||
};
|
||||
let mut length = std::mem::size_of::<libc::ucred>() as libc::socklen_t;
|
||||
|
||||
// SAFETY: `credentials` and `length` point to initialized, correctly sized
|
||||
@@ -1940,7 +2113,10 @@ mod tests {
|
||||
let (mut event_loop, mut state) = deadline_loop();
|
||||
let timer = state.timer.as_ref().unwrap();
|
||||
timer.update(None);
|
||||
assert!(timer.armed.borrow().is_none(), "idle registers no timeout source");
|
||||
assert!(
|
||||
timer.armed.borrow().is_none(),
|
||||
"idle registers no timeout source"
|
||||
);
|
||||
event_loop.dispatch(Duration::ZERO, &mut state).unwrap();
|
||||
assert_eq!(state.fired, 0);
|
||||
|
||||
@@ -1977,7 +2153,10 @@ mod tests {
|
||||
state.timer.as_ref().unwrap().update(Some(due));
|
||||
state.timer.as_ref().unwrap().update(None);
|
||||
event_loop.dispatch(Duration::ZERO, &mut state).unwrap();
|
||||
assert_eq!(state.fired, 0, "cancelling the due timer removes its wakeup");
|
||||
assert_eq!(
|
||||
state.fired, 0,
|
||||
"cancelling the due timer removes its wakeup"
|
||||
);
|
||||
assert!(state.timer.as_ref().unwrap().armed.borrow().is_none());
|
||||
}
|
||||
|
||||
@@ -1991,7 +2170,10 @@ mod tests {
|
||||
.update(Some(Instant::now() - Duration::from_secs(1)));
|
||||
drop(state.timer.take());
|
||||
event_loop.dispatch(Duration::ZERO, &mut state).unwrap();
|
||||
assert_eq!(state.fired, 0, "dropping the observer removes its timer callback");
|
||||
assert_eq!(
|
||||
state.fired, 0,
|
||||
"dropping the observer removes its timer callback"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2034,7 +2216,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn parses_direction_arguments() {
|
||||
assert_eq!(direction_arg(&json!({ "direction": "left" })).unwrap(), Direction::Left);
|
||||
assert_eq!(
|
||||
direction_arg(&json!({ "direction": "left" })).unwrap(),
|
||||
Direction::Left
|
||||
);
|
||||
assert!(direction_arg(&json!({ "direction": "diagonal" })).is_err());
|
||||
}
|
||||
|
||||
@@ -2084,7 +2269,10 @@ mod tests {
|
||||
assert!(try_acquire_connection(&active).is_none());
|
||||
let deadline = Instant::now() + transport::FRAME_TIMEOUT + Duration::from_secs(3);
|
||||
while active.load(Ordering::Acquire) != 0 {
|
||||
assert!(Instant::now() < deadline, "silent peers retained connection slots");
|
||||
assert!(
|
||||
Instant::now() < deadline,
|
||||
"silent peers retained connection slots"
|
||||
);
|
||||
thread::sleep(Duration::from_millis(20));
|
||||
}
|
||||
assert!(try_acquire_connection(&active).is_some());
|
||||
@@ -2104,7 +2292,9 @@ mod tests {
|
||||
let worker = thread::spawn(move || serve_connection(server, sender, signal, 1));
|
||||
let invalid = b"not-json";
|
||||
|
||||
client.write_all(&(invalid.len() as u32).to_be_bytes()).unwrap();
|
||||
client
|
||||
.write_all(&(invalid.len() as u32).to_be_bytes())
|
||||
.unwrap();
|
||||
client.write_all(invalid).unwrap();
|
||||
client.shutdown(Shutdown::Write).unwrap();
|
||||
client
|
||||
@@ -2173,7 +2363,10 @@ mod tests {
|
||||
assert!(!first_directory.exists());
|
||||
assert!(second.path.parent().unwrap().is_dir());
|
||||
assert!(default.symlink_metadata().unwrap().file_type().is_socket());
|
||||
assert_eq!(bind_listener(&default).unwrap_err().kind(), io::ErrorKind::AddrInUse);
|
||||
assert_eq!(
|
||||
bind_listener(&default).unwrap_err().kind(),
|
||||
io::ErrorKind::AddrInUse
|
||||
);
|
||||
drop(host);
|
||||
}
|
||||
|
||||
@@ -2213,7 +2406,10 @@ mod tests {
|
||||
fs::set_permissions(&outside, fs::Permissions::from_mode(0o755)).unwrap();
|
||||
std::os::unix::fs::symlink(&outside, default.parent().unwrap().join("instances")).unwrap();
|
||||
assert!(Endpoint::instance_at(default).is_err());
|
||||
assert_eq!(fs::metadata(outside).unwrap().permissions().mode() & 0o777, 0o755);
|
||||
assert_eq!(
|
||||
fs::metadata(outside).unwrap().permissions().mode() & 0o777,
|
||||
0o755
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2245,7 +2441,10 @@ mod tests {
|
||||
let socket = root.join("control.sock");
|
||||
let first = bind_listener(&socket).unwrap();
|
||||
|
||||
assert_eq!(bind_listener(&socket).unwrap_err().kind(), io::ErrorKind::AddrInUse);
|
||||
assert_eq!(
|
||||
bind_listener(&socket).unwrap_err().kind(),
|
||||
io::ErrorKind::AddrInUse
|
||||
);
|
||||
drop(first);
|
||||
let replacement = bind_listener(&socket).unwrap();
|
||||
|
||||
@@ -2261,7 +2460,10 @@ mod tests {
|
||||
let socket = root.join("control.sock");
|
||||
fs::write(&socket, b"keep").unwrap();
|
||||
|
||||
assert_eq!(bind_listener(&socket).unwrap_err().kind(), io::ErrorKind::AddrInUse);
|
||||
assert_eq!(
|
||||
bind_listener(&socket).unwrap_err().kind(),
|
||||
io::ErrorKind::AddrInUse
|
||||
);
|
||||
assert_eq!(fs::read(&socket).unwrap(), b"keep");
|
||||
fs::remove_dir_all(root).unwrap();
|
||||
}
|
||||
@@ -2296,7 +2498,9 @@ mod tests {
|
||||
#[test]
|
||||
fn stalled_event_socket_write_expires_on_the_worker() {
|
||||
let (_client, mut server) = UnixStream::pair().unwrap();
|
||||
server.set_write_timeout(Some(Duration::from_millis(50))).unwrap();
|
||||
server
|
||||
.set_write_timeout(Some(Duration::from_millis(50)))
|
||||
.unwrap();
|
||||
let (sender, receiver) = sync_channel(1);
|
||||
let (finished, completion) = sync_channel(1);
|
||||
sender
|
||||
|
||||
@@ -25,7 +25,9 @@ impl VerifiedOwner {
|
||||
let mut next = self.owner.pid;
|
||||
let mut child = None;
|
||||
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 {
|
||||
break;
|
||||
}
|
||||
@@ -59,7 +61,10 @@ impl VerifiedOwner {
|
||||
pub(crate) struct Monitor(Arc<(Mutex<BTreeMap<Owner, OwnedFd>>, Condvar)>);
|
||||
impl Monitor {
|
||||
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();
|
||||
std::thread::Builder::new()
|
||||
.name("ferese-activity-owners".into())
|
||||
@@ -104,7 +109,11 @@ impl Monitor {
|
||||
.expect("activity owners")
|
||||
.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_pid = u32::try_from(peer.pid).map_err(|_| io::Error::other("invalid 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 (birth, _) = identity(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"));
|
||||
}
|
||||
let metadata = std::fs::metadata(format!("/proc/{pid}"))?;
|
||||
@@ -224,7 +236,11 @@ fn parse_stat(stat: &str) -> io::Result<(u64, u32)> {
|
||||
Ok((birth, parent))
|
||||
}
|
||||
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;
|
||||
// SAFETY: both output buffers have the sizes expected by SO_PEERCRED.
|
||||
if unsafe {
|
||||
@@ -274,7 +290,10 @@ mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
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 mut verified = VerifiedOwner {
|
||||
owner,
|
||||
@@ -283,7 +302,11 @@ mod tests {
|
||||
};
|
||||
verified.capture_ancestors();
|
||||
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.wait().unwrap();
|
||||
assert!(verified.ancestor_pids().next().is_none());
|
||||
@@ -334,7 +357,10 @@ mod tests {
|
||||
fn owner_that_exits_in_the_dispatch_queue_is_rejected() {
|
||||
let (sender, _receiver) = channel::sync_channel(16);
|
||||
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();
|
||||
child.kill().unwrap();
|
||||
child.wait().unwrap();
|
||||
@@ -370,7 +396,10 @@ mod tests {
|
||||
assert_eq!(peer.owner.pid, std::process::id());
|
||||
let (parent, _) = monitor.verify(&left, true).unwrap();
|
||||
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();
|
||||
assert!(!exited(&fd));
|
||||
child.wait().unwrap();
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -60,7 +63,10 @@ fn read_request_with_budget(
|
||||
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.
|
||||
let previous = stream.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());
|
||||
|
||||
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);
|
||||
let request = Request {
|
||||
version: 1,
|
||||
@@ -112,8 +119,12 @@ mod tests {
|
||||
std::thread::sleep(Duration::from_millis(30));
|
||||
}
|
||||
});
|
||||
let error =
|
||||
read_request_with_budget(&server, Some(Duration::from_secs(1)), Duration::from_millis(70)).unwrap_err();
|
||||
let error = read_request_with_budget(
|
||||
&server,
|
||||
Some(Duration::from_secs(1)),
|
||||
Duration::from_millis(70),
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
matches!(error, FrameError::Io(error) if matches!(error.kind(), io::ErrorKind::TimedOut | io::ErrorKind::WouldBlock))
|
||||
);
|
||||
|
||||
@@ -21,11 +21,21 @@ pub(crate) struct 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 {
|
||||
return Err("Event subscriber limit reached");
|
||||
}
|
||||
self.subscribers.insert(owner, Subscriber { sender, initial: true });
|
||||
self.subscribers.insert(
|
||||
owner,
|
||||
Subscriber {
|
||||
sender,
|
||||
initial: true,
|
||||
},
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -47,7 +57,12 @@ impl Subscribers {
|
||||
.as_ref()
|
||||
.map(|previous| snapshot.changes_since(previous))
|
||||
.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;
|
||||
}
|
||||
self.generation = self.generation.wrapping_add(1);
|
||||
@@ -65,7 +80,11 @@ impl Subscribers {
|
||||
if let Some(last) = events.last_mut() {
|
||||
Arc::get_mut(last).unwrap().last = true;
|
||||
}
|
||||
let initial = self.subscribers.values().any(|subscriber| subscriber.initial).then(|| {
|
||||
let initial = self
|
||||
.subscribers
|
||||
.values()
|
||||
.any(|subscriber| subscriber.initial)
|
||||
.then(|| {
|
||||
Arc::new(Event {
|
||||
version: VERSION,
|
||||
generation: self.generation,
|
||||
@@ -77,7 +96,10 @@ impl Subscribers {
|
||||
});
|
||||
self.subscribers.retain(|_, subscriber| {
|
||||
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 {
|
||||
events
|
||||
.iter()
|
||||
@@ -137,7 +159,10 @@ mod tests {
|
||||
hub.subscribe(1, sender).unwrap();
|
||||
let mut state = snapshot();
|
||||
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());
|
||||
assert!(matches!(receive.try_recv(), Err(TryRecvError::Empty)));
|
||||
state.outputs = json!([{"id": 2}]);
|
||||
@@ -146,7 +171,11 @@ mod tests {
|
||||
hub.publish(state);
|
||||
let events = receive.try_iter().collect::<Vec<_>>();
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -171,7 +200,10 @@ mod tests {
|
||||
));
|
||||
assert_eq!(hub.subscribers.len(), 1);
|
||||
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);
|
||||
assert!(!hub.active());
|
||||
assert!(hub.previous.is_none());
|
||||
@@ -209,7 +241,11 @@ mod tests {
|
||||
hub.publish(state);
|
||||
let events = receive.try_iter().collect::<Vec<_>>();
|
||||
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].last);
|
||||
}
|
||||
@@ -229,7 +265,8 @@ mod tests {
|
||||
let mut event_loop = EventLoop::try_new().unwrap();
|
||||
let mut config = crate::config::Config::default().runtime_config().unwrap();
|
||||
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 {
|
||||
output: Output::new(
|
||||
name.into(),
|
||||
@@ -273,7 +310,11 @@ mod tests {
|
||||
state.publish_ipc_events();
|
||||
let events = receive.try_iter().collect::<Vec<_>>();
|
||||
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();
|
||||
assert_eq!(final_state.outputs.as_array().unwrap().len(), 2);
|
||||
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);
|
||||
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
|
||||
.dispatch(std::time::Duration::from_millis(10), &mut state)
|
||||
.unwrap();
|
||||
|
||||
+12
-5
@@ -80,8 +80,8 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let launch =
|
||||
LaunchConfig::from_environment().map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, error))?;
|
||||
let launch = LaunchConfig::from_environment()
|
||||
.map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, error))?;
|
||||
let config_path = config::config_path();
|
||||
let startup = config::startup(config_path.as_deref())?;
|
||||
if let Some(error) = &startup.error {
|
||||
@@ -120,7 +120,9 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
value
|
||||
});
|
||||
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)?;
|
||||
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);
|
||||
state.process_pending_dmabuf_imports();
|
||||
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.
|
||||
let wallpaper_changed = state.wallpaper.poll();
|
||||
// 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();
|
||||
}
|
||||
|
||||
|
||||
@@ -108,8 +108,14 @@ impl RenderMetrics {
|
||||
.unwrap_or(0);
|
||||
self.frame_times.make_contiguous().sort_unstable();
|
||||
let percentile = |percent: usize| {
|
||||
let index = (self.frame_times.len() * percent).div_ceil(100).saturating_sub(1);
|
||||
self.frame_times.get(index).copied().unwrap_or_default().as_micros()
|
||||
let index = (self.frame_times.len() * percent)
|
||||
.div_ceil(100)
|
||||
.saturating_sub(1);
|
||||
self.frame_times
|
||||
.get(index)
|
||||
.copied()
|
||||
.unwrap_or_default()
|
||||
.as_micros()
|
||||
};
|
||||
tracing::info!(
|
||||
target: "ferese::render",
|
||||
@@ -145,7 +151,8 @@ impl RenderMetrics {
|
||||
}
|
||||
|
||||
fn rectangle_area(rectangle: &Rectangle<i32, Physical>) -> u64 {
|
||||
u64::try_from(rectangle.size.w.max(0)).unwrap_or(0) * u64::try_from(rectangle.size.h.max(0)).unwrap_or(0)
|
||||
u64::try_from(rectangle.size.w.max(0)).unwrap_or(0)
|
||||
* u64::try_from(rectangle.size.h.max(0)).unwrap_or(0)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -240,8 +247,14 @@ mod tests {
|
||||
for micros in 0..600 {
|
||||
metrics.record_frame(Duration::from_micros(micros), &[], 0, FrameEffectMetrics);
|
||||
}
|
||||
assert_eq!(metrics.frame_times.front(), Some(&Duration::from_micros(88)));
|
||||
assert_eq!(metrics.frame_times.back(), Some(&Duration::from_micros(599)));
|
||||
assert_eq!(
|
||||
metrics.frame_times.front(),
|
||||
Some(&Duration::from_micros(88))
|
||||
);
|
||||
assert_eq!(
|
||||
metrics.frame_times.back(),
|
||||
Some(&Duration::from_micros(599))
|
||||
);
|
||||
assert!(
|
||||
metrics
|
||||
.frame_times
|
||||
|
||||
@@ -26,7 +26,10 @@ impl Identity {
|
||||
pub fn from_edid(edid: &[u8]) -> Option<Self> {
|
||||
if edid.len() < 128
|
||||
|| 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;
|
||||
}
|
||||
@@ -62,7 +65,11 @@ impl Identity {
|
||||
if serial.is_none() && numeric_serial != 0 && numeric_serial != u32::MAX {
|
||||
serial = Some(format!("{numeric_serial:08X}"));
|
||||
}
|
||||
Some(Self { make, model, serial })
|
||||
Some(Self {
|
||||
make,
|
||||
model,
|
||||
serial,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn base(&self) -> String {
|
||||
@@ -90,8 +97,12 @@ impl IdentityRegistry {
|
||||
*counts.entry(identity.base()).or_insert(0) += 1;
|
||||
}
|
||||
}
|
||||
self.ambiguous
|
||||
.extend(counts.into_iter().filter(|(_, count)| *count > 1).map(|(base, _)| base));
|
||||
self.ambiguous.extend(
|
||||
counts
|
||||
.into_iter()
|
||||
.filter(|(_, count)| *count > 1)
|
||||
.map(|(base, _)| base),
|
||||
);
|
||||
// Prefer already-known identities, but never permit duplicate runtime IDs.
|
||||
let mut used = HashSet::new();
|
||||
let mut ordered = monitors.iter().enumerate().collect::<Vec<_>>();
|
||||
@@ -107,12 +118,15 @@ impl IdentityRegistry {
|
||||
.known
|
||||
.get(key)
|
||||
.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)
|
||||
})
|
||||
.cloned()
|
||||
.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())
|
||||
{
|
||||
base.clone().unwrap()
|
||||
@@ -208,7 +222,10 @@ mod tests {
|
||||
edid[10..12].copy_from_slice(&0x1234u16.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)));
|
||||
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;
|
||||
assert!(Identity::from_edid(&edid).is_none());
|
||||
}
|
||||
|
||||
@@ -16,7 +16,9 @@ pub(crate) struct Monitor {
|
||||
|
||||
impl Monitor {
|
||||
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;
|
||||
}
|
||||
if self.profile.as_ref().is_some_and(|name| {
|
||||
!profiles
|
||||
.iter()
|
||||
.any(|profile| profile.name == *name && profile_matches(profile, monitors, lid_closed))
|
||||
!profiles.iter().any(|profile| {
|
||||
profile.name == *name && profile_matches(profile, monitors, lid_closed)
|
||||
})
|
||||
}) {
|
||||
self.profile = None;
|
||||
}
|
||||
@@ -79,7 +81,9 @@ fn profile_matches(profile: &OutputProfile, monitors: &[Monitor], lid_closed: bo
|
||||
}) {
|
||||
return false;
|
||||
}
|
||||
profile.lid_closed.is_none_or(|required| required == lid_closed)
|
||||
profile
|
||||
.lid_closed
|
||||
.is_none_or(|required| required == lid_closed)
|
||||
&& profile
|
||||
.outputs
|
||||
.iter()
|
||||
@@ -119,9 +123,9 @@ pub(crate) fn select_profile(
|
||||
.profile
|
||||
.as_ref()
|
||||
.and_then(|name| {
|
||||
profiles
|
||||
.iter()
|
||||
.find(|profile| profile.name == *name && profile_matches(profile, monitors, lid_closed))
|
||||
profiles.iter().find(|profile| {
|
||||
profile.name == *name && profile_matches(profile, monitors, lid_closed)
|
||||
})
|
||||
})
|
||||
.or_else(|| {
|
||||
profiles
|
||||
@@ -130,7 +134,11 @@ pub(crate) fn select_profile(
|
||||
.filter(|(_, profile)| profile_matches(profile, monitors, lid_closed))
|
||||
.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),
|
||||
)
|
||||
})
|
||||
@@ -140,7 +148,9 @@ pub(crate) fn select_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 external = monitors.iter().any(|monitor| monitor.usable && !monitor.internal);
|
||||
let external = monitors
|
||||
.iter()
|
||||
.any(|monitor| monitor.usable && !monitor.internal);
|
||||
let mut outputs = monitors
|
||||
.iter()
|
||||
.map(|monitor| {
|
||||
@@ -229,7 +239,11 @@ pub(crate) fn restore_configuration(
|
||||
.outputs
|
||||
.retain(|output| monitors.iter().any(|monitor| monitor.key == output.key));
|
||||
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());
|
||||
settings.enabled = false;
|
||||
restored.outputs.push(DesiredOutput {
|
||||
@@ -254,7 +268,11 @@ pub(crate) fn restore_configuration(
|
||||
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());
|
||||
}
|
||||
restored
|
||||
@@ -278,7 +296,8 @@ pub(crate) fn changed_outputs<'a>(
|
||||
let next = new.outputs.iter().find(|candidate| candidate.key == key);
|
||||
let same = match (previous, 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,
|
||||
_ => false,
|
||||
@@ -294,7 +313,11 @@ pub(crate) struct 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
|
||||
}
|
||||
@@ -341,7 +364,9 @@ pub(crate) fn arrange_positions(
|
||||
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 {
|
||||
let fits = |position: [i32; 2]| {
|
||||
let (x, y, w, h) = (
|
||||
@@ -351,7 +376,10 @@ pub(crate) fn arrange_positions(
|
||||
placement.size.1 as i64,
|
||||
);
|
||||
!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));
|
||||
@@ -463,7 +491,10 @@ mod tests {
|
||||
fn profile(name: &str, outputs: &[&str], layout: OutputLayout) -> OutputProfile {
|
||||
OutputProfile {
|
||||
name: name.into(),
|
||||
outputs: outputs.iter().map(|name| defaults((*name).into())).collect(),
|
||||
outputs: outputs
|
||||
.iter()
|
||||
.map(|name| defaults((*name).into()))
|
||||
.collect(),
|
||||
layout,
|
||||
confirm_timeout: 15,
|
||||
lid_policy: LidPolicy::DockOrSuspend,
|
||||
@@ -559,7 +590,11 @@ mod tests {
|
||||
&ManualOverride::default(),
|
||||
);
|
||||
assert_eq!(
|
||||
desired.outputs.iter().filter(|output| output.settings.enabled).count(),
|
||||
desired
|
||||
.outputs
|
||||
.iter()
|
||||
.filter(|output| output.settings.enabled)
|
||||
.count(),
|
||||
count
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -574,7 +609,12 @@ mod tests {
|
||||
let missing = [profile("absent", &["DP-9"], OutputLayout::ExternalOnly)];
|
||||
let fallback = select_profile(&monitors, false, &missing, &ManualOverride::default());
|
||||
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 mut disabled = profile("disabled", &["eDP-1"], OutputLayout::Extend);
|
||||
disabled.outputs[0].enabled = false;
|
||||
@@ -631,13 +671,23 @@ mod tests {
|
||||
let monitors = [monitor("eDP-1", true), monitor("DP-1", false)];
|
||||
let mut mirror = profile("mirror", &["eDP-1", "DP-1"], OutputLayout::Mirror);
|
||||
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"));
|
||||
let removed = select_profile(&monitors[..1], false, &[mirror], &ManualOverride::default());
|
||||
assert!(removed.outputs[0].mirror_source.is_none());
|
||||
assert!(removed.outputs[0].settings.enabled);
|
||||
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"]);
|
||||
}
|
||||
|
||||
@@ -648,7 +698,12 @@ mod tests {
|
||||
mobile.outputs[1].required = false;
|
||||
mobile.lid_closed = Some(false);
|
||||
assert_eq!(
|
||||
select_profile(&monitors, false, &[mobile.clone()], &ManualOverride::default())
|
||||
select_profile(
|
||||
&monitors,
|
||||
false,
|
||||
&[mobile.clone()],
|
||||
&ManualOverride::default()
|
||||
)
|
||||
.profile
|
||||
.as_deref(),
|
||||
Some("mobile")
|
||||
@@ -679,7 +734,11 @@ mod tests {
|
||||
assert_eq!(restored.outputs.len(), 1);
|
||||
assert!(restored.outputs[0].settings.enabled);
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -152,7 +152,12 @@ mod tests {
|
||||
let inventory = |active: &BTreeSet<u8>| {
|
||||
active
|
||||
.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<_>>()
|
||||
};
|
||||
for rollback_fails in [false, true] {
|
||||
@@ -184,8 +189,14 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
if rollback_fails {
|
||||
assert_eq!(plan.outputs.connected_outputs().collect::<Vec<_>>(), vec![OutputId(2)]);
|
||||
assert_eq!(plan.outputs.output_for_workspace(WorkspaceId(1)), Some(OutputId(2)));
|
||||
assert_eq!(
|
||||
plan.outputs.connected_outputs().collect::<Vec<_>>(),
|
||||
vec![OutputId(2)]
|
||||
);
|
||||
assert_eq!(
|
||||
plan.outputs.output_for_workspace(WorkspaceId(1)),
|
||||
Some(OutputId(2))
|
||||
);
|
||||
} else {
|
||||
assert_eq!(plan.outputs, published);
|
||||
assert!(plan.affected_outputs.is_empty());
|
||||
|
||||
+222
-61
@@ -41,10 +41,13 @@ pub(crate) fn init_font_loader(
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
use smithay::reexports::calloop::channel;
|
||||
let (results, source) = channel::sync_channel::<(String, Option<FontArc>)>(1);
|
||||
event_loop.handle().insert_source(source, |event, _, state| {
|
||||
event_loop
|
||||
.handle()
|
||||
.insert_source(source, |event, _, state| {
|
||||
if let channel::Event::Msg((family, font)) = event
|
||||
&& state.overview.complete_font_load(&family, font)
|
||||
&& (state.overview.is_presenting() || state.tab_bars.values().any(|bars| !bars.is_empty()))
|
||||
&& (state.overview.is_presenting()
|
||||
|| state.tab_bars.values().any(|bars| !bars.is_empty()))
|
||||
{
|
||||
crate::backends::direct::render_all(state);
|
||||
}
|
||||
@@ -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 OverviewStateLabel = HashMap<String, HashMap<LabelStyle, LabelBuffer>>;
|
||||
|
||||
@@ -170,11 +176,19 @@ pub(crate) fn workspace_strip(bounds: Rect) -> Rect {
|
||||
fn window_area(bounds: Rect) -> Rect {
|
||||
let strip = workspace_strip(bounds);
|
||||
let top = strip.y + strip.height + 16.0;
|
||||
Rect::new(bounds.x, top, bounds.width, (bounds.y + bounds.height - top).max(1.0))
|
||||
Rect::new(
|
||||
bounds.x,
|
||||
top,
|
||||
bounds.width,
|
||||
(bounds.y + bounds.height - top).max(1.0),
|
||||
)
|
||||
}
|
||||
|
||||
fn contains(rect: Rect, point: Point<f64, Logical>) -> bool {
|
||||
point.x >= rect.x && point.x < rect.x + rect.width && point.y >= rect.y && point.y < rect.y + rect.height
|
||||
point.x >= rect.x
|
||||
&& point.x < rect.x + rect.width
|
||||
&& point.y >= rect.y
|
||||
&& point.y < rect.y + rect.height
|
||||
}
|
||||
|
||||
fn strip_capacity(strip: Rect) -> usize {
|
||||
@@ -192,11 +206,10 @@ impl OverviewState {
|
||||
pub(crate) fn needs_tick(&self) -> bool {
|
||||
self.motion.opacity.current != self.motion.opacity.target
|
||||
|| self.motion.opacity.velocity != 0.0
|
||||
|| self
|
||||
.motion
|
||||
.presentations
|
||||
.values()
|
||||
.any(|rect| rect.current != rect.target || rect.velocity != ferese_animation::RectVelocity::default())
|
||||
|| self.motion.presentations.values().any(|rect| {
|
||||
rect.current != rect.target
|
||||
|| rect.velocity != ferese_animation::RectVelocity::default()
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn is_animating(&self, spring: SpringConfig) -> bool {
|
||||
@@ -246,12 +259,25 @@ impl OverviewState {
|
||||
text: &str,
|
||||
scale: f64,
|
||||
color: [f32; 4],
|
||||
) -> Option<(MemoryRenderBuffer, smithay::utils::Size<i32, smithay::utils::Buffer>)> {
|
||||
) -> Option<(
|
||||
MemoryRenderBuffer,
|
||||
smithay::utils::Size<i32, smithay::utils::Buffer>,
|
||||
)> {
|
||||
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> {
|
||||
let key = (scale.to_bits(), font_size.to_bits(), color.map(f32::to_bits));
|
||||
fn label_with_size(
|
||||
&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)) {
|
||||
return Some(buffer.clone());
|
||||
}
|
||||
@@ -269,7 +295,10 @@ impl OverviewState {
|
||||
let mut pen = 2.0;
|
||||
for ch in text.chars() {
|
||||
let id = font.glyph_id(ch);
|
||||
let glyph = id.with_scale_and_position(scaled.scale(), ab_glyph::point(pen, scaled.ascent() + 1.0));
|
||||
let glyph = id.with_scale_and_position(
|
||||
scaled.scale(),
|
||||
ab_glyph::point(pen, scaled.ascent() + 1.0),
|
||||
);
|
||||
pen += scaled.h_advance(id);
|
||||
if let Some(outline) = font.outline_glyph(glyph) {
|
||||
let bounds = outline.px_bounds();
|
||||
@@ -289,8 +318,14 @@ impl OverviewState {
|
||||
});
|
||||
}
|
||||
}
|
||||
let buffer =
|
||||
MemoryRenderBuffer::from_slice(&pixels, Fourcc::Abgr8888, (width, height), 1, Transform::Normal, None);
|
||||
let buffer = MemoryRenderBuffer::from_slice(
|
||||
&pixels,
|
||||
Fourcc::Abgr8888,
|
||||
(width, height),
|
||||
1,
|
||||
Transform::Normal,
|
||||
None,
|
||||
);
|
||||
if self.label_count >= 128 {
|
||||
self.labels.clear();
|
||||
self.label_count = 0;
|
||||
@@ -318,12 +353,19 @@ impl OverviewState {
|
||||
}
|
||||
|
||||
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
|
||||
.selected
|
||||
.filter(visible)
|
||||
.or_else(|| preferred.filter(visible))
|
||||
.or_else(|| windows.iter().copied().filter(visible).min_by_key(|id| id.0));
|
||||
.or_else(|| {
|
||||
windows
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(visible)
|
||||
.min_by_key(|id| id.0)
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn selection_state(&self) -> (Option<WindowId>, Option<WorkspaceId>) {
|
||||
@@ -363,9 +405,13 @@ impl OverviewState {
|
||||
.filter(|(id, _)| Some(**id) != self.selected)
|
||||
.filter_map(|(id, presentation)| {
|
||||
let center = rect_center(presentation.current);
|
||||
directional_distance(direction, current_center, center).map(|distance| (*id, distance))
|
||||
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);
|
||||
let Some(next) = next else {
|
||||
return false;
|
||||
@@ -385,9 +431,15 @@ impl OverviewState {
|
||||
&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 {
|
||||
self.motion.presentations.entry(window.id).or_insert_with(|| {
|
||||
self.motion
|
||||
.presentations
|
||||
.entry(window.id)
|
||||
.or_insert_with(|| {
|
||||
// Opening scales the final presentation after overview. Remove
|
||||
// it here so the same opening motion is not applied twice.
|
||||
let scale = 0.97 + 0.03 * window.opacity.current;
|
||||
@@ -398,7 +450,8 @@ impl OverviewState {
|
||||
1.0 / scale,
|
||||
-0.03 * window.opacity.velocity / (scale * scale),
|
||||
);
|
||||
bounds.current = crate::presentation::scaled_visual_rect(bounds.current, 1.0 / scale);
|
||||
bounds.current =
|
||||
crate::presentation::scaled_visual_rect(bounds.current, 1.0 / scale);
|
||||
bounds.velocity.x /= OVERVIEW_MOTION_SPEED;
|
||||
bounds.velocity.y /= OVERVIEW_MOTION_SPEED;
|
||||
bounds.velocity.width /= OVERVIEW_MOTION_SPEED;
|
||||
@@ -428,7 +481,9 @@ impl OverviewState {
|
||||
}
|
||||
|
||||
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 {
|
||||
let presentation = self
|
||||
@@ -446,7 +501,12 @@ impl OverviewState {
|
||||
.selected
|
||||
.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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
.space
|
||||
.outputs()
|
||||
@@ -509,7 +577,8 @@ impl Ferese {
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.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()
|
||||
@@ -523,7 +592,12 @@ impl Ferese {
|
||||
.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)
|
||||
}
|
||||
|
||||
@@ -537,7 +611,11 @@ impl Ferese {
|
||||
let record = self.windows.record(id)?;
|
||||
let limit = ((width - 16.0) / 8.0).clamp(1.0, 48.0) as usize;
|
||||
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)
|
||||
&& label.revision == record.metadata_revision
|
||||
@@ -563,7 +641,9 @@ impl Ferese {
|
||||
}
|
||||
|
||||
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(
|
||||
id,
|
||||
WindowLabel {
|
||||
@@ -604,9 +684,15 @@ impl Ferese {
|
||||
&mut self,
|
||||
index: u32,
|
||||
scale: f64,
|
||||
) -> Option<(MemoryRenderBuffer, smithay::utils::Size<i32, smithay::utils::Buffer>)> {
|
||||
self.overview
|
||||
.label(&index.to_string(), scale, self.theme_settings.text_primary_color.0)
|
||||
) -> Option<(
|
||||
MemoryRenderBuffer,
|
||||
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) {
|
||||
@@ -676,7 +762,10 @@ impl Ferese {
|
||||
if !self.overview.is_active() {
|
||||
if self.overview.is_presenting() {
|
||||
let normal = self.normal_window_rects();
|
||||
self.overview.motion.presentations.retain(|id, presentation| {
|
||||
self.overview
|
||||
.motion
|
||||
.presentations
|
||||
.retain(|id, presentation| {
|
||||
if let Some(target) = normal.get(id) {
|
||||
presentation.set_target(*target);
|
||||
true
|
||||
@@ -691,7 +780,13 @@ impl Ferese {
|
||||
let targets = self.overview_targets();
|
||||
self.overview.retarget(targets, self.animations_enabled());
|
||||
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) {
|
||||
self.overview.select_window(id);
|
||||
}
|
||||
@@ -711,7 +806,8 @@ impl Ferese {
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
self.overview.select_workspace_windows(&windows, self.focused_window);
|
||||
self.overview
|
||||
.select_workspace_windows(&windows, self.focused_window);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -719,7 +815,12 @@ impl Ferese {
|
||||
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)?;
|
||||
if !self.overview.is_presenting() && self.windows.record(id)?.opening.is_none() {
|
||||
let presented = self.presented_window_rect(id)?;
|
||||
@@ -732,7 +833,11 @@ impl Ferese {
|
||||
};
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -780,7 +885,8 @@ impl Ferese {
|
||||
.copied()
|
||||
.unwrap_or_else(|| selected_index.saturating_sub(capacity - 1))
|
||||
.min(max_offset);
|
||||
let card_width = ((strip.width - 24.0 - WORKSPACE_CARD_GAP * capacity.saturating_sub(1) as f64)
|
||||
let card_width =
|
||||
((strip.width - 24.0 - WORKSPACE_CARD_GAP * capacity.saturating_sub(1) as f64)
|
||||
/ capacity as f64)
|
||||
.clamp(1.0, 160.0);
|
||||
let visible_count = workspaces.len().saturating_sub(offset).min(capacity);
|
||||
@@ -791,7 +897,10 @@ impl Ferese {
|
||||
.take(capacity)
|
||||
.enumerate()
|
||||
.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(
|
||||
start_x + index as f64 * (card_width + WORKSPACE_CARD_GAP),
|
||||
@@ -861,8 +970,11 @@ impl Ferese {
|
||||
self.advance_animations(std::time::Instant::now());
|
||||
let previous = self.output_workspaces.active_workspace(output_id);
|
||||
if let Ok(
|
||||
ferese_core::WorkspaceSwitch::Activated(owner) | ferese_core::WorkspaceSwitch::FocusedExisting(owner),
|
||||
) = self.output_workspaces.switch_workspace(output_id, card.workspace)
|
||||
ferese_core::WorkspaceSwitch::Activated(owner)
|
||||
| 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);
|
||||
// Existing monitor ownership wins; never steal another output's workspace.
|
||||
@@ -889,7 +1001,9 @@ impl Ferese {
|
||||
};
|
||||
let capacity = strip_capacity(workspace_strip(bounds));
|
||||
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
|
||||
.first()
|
||||
.and_then(|card| ordered.iter().position(|id| *id == card.workspace))
|
||||
@@ -933,12 +1047,17 @@ impl Ferese {
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
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;
|
||||
}
|
||||
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));
|
||||
for workspace in visible {
|
||||
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
|
||||
// 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() {
|
||||
return Vec::new();
|
||||
}
|
||||
@@ -996,8 +1120,10 @@ fn preview_layout(bounds: Rect, windows: &[(WindowId, Rect)], margin: f64, gap:
|
||||
let cell_size = |columns: usize| {
|
||||
let rows = windows.len().div_ceil(columns);
|
||||
(
|
||||
((content_width - gap * columns.saturating_sub(1) as f64) / columns as f64).clamp(1.0, 480.0),
|
||||
((content_height - gap * rows.saturating_sub(1) as f64) / rows as f64).clamp(1.0, 420.0),
|
||||
((content_width - gap * columns.saturating_sub(1) as f64) / columns as f64)
|
||||
.clamp(1.0, 480.0),
|
||||
((content_height - gap * rows.saturating_sub(1) as f64) / rows as f64)
|
||||
.clamp(1.0, 420.0),
|
||||
)
|
||||
};
|
||||
let score = |columns: usize| {
|
||||
@@ -1039,8 +1165,10 @@ fn preview_layout(bounds: Rect, windows: &[(WindowId, Rect)], margin: f64, gap:
|
||||
let width = (source.width * scale).max(1.0);
|
||||
let height = (source.height * scale).max(1.0);
|
||||
let row_count = (windows.len() - row * columns).min(columns);
|
||||
let row_width = cell_width * row_count as f64 + gap * row_count.saturating_sub(1) as f64;
|
||||
let cell_x = bounds.x + (bounds.width - row_width) * 0.5 + column as f64 * (cell_width + gap);
|
||||
let row_width =
|
||||
cell_width * row_count as f64 + gap * row_count.saturating_sub(1) as f64;
|
||||
let cell_x =
|
||||
bounds.x + (bounds.width - row_width) * 0.5 + column as f64 * (cell_width + gap);
|
||||
let cell_y = origin_y + row as f64 * (cell_height + gap);
|
||||
let target = Rect::new(
|
||||
cell_x + (cell_width - width) / 2.0,
|
||||
@@ -1058,7 +1186,11 @@ fn rect_center(rect: Rect) -> (f64, f64) {
|
||||
(rect.x + rect.width / 2.0, rect.y + rect.height / 2.0)
|
||||
}
|
||||
|
||||
fn directional_distance(direction: Direction, current: (f64, f64), candidate: (f64, f64)) -> Option<f64> {
|
||||
fn directional_distance(
|
||||
direction: Direction,
|
||||
current: (f64, f64),
|
||||
candidate: (f64, f64),
|
||||
) -> Option<f64> {
|
||||
let delta_x = candidate.0 - current.0;
|
||||
let delta_y = candidate.1 - current.1;
|
||||
let (primary, secondary) = match direction {
|
||||
@@ -1086,7 +1218,10 @@ impl OverviewMotion {
|
||||
pub(crate) fn is_animating(&self, spring: SpringConfig) -> bool {
|
||||
(self.opacity.current - self.opacity.target).abs() > 0.001
|
||||
|| 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 {
|
||||
@@ -1113,7 +1248,12 @@ impl OverviewMotion {
|
||||
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 mut active_animation = false;
|
||||
@@ -1194,7 +1334,9 @@ mod tests {
|
||||
Some(id),
|
||||
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.width - normal.width).abs() < 1e-10);
|
||||
assert!((sampled.velocity.x - velocity.x).abs() < 1e-10);
|
||||
@@ -1265,7 +1407,10 @@ mod tests {
|
||||
requests.request(family);
|
||||
}
|
||||
receiver.try_recv().unwrap();
|
||||
assert!(matches!(receiver.try_recv(), Err(mpsc::TryRecvError::Empty)));
|
||||
assert!(matches!(
|
||||
receiver.try_recv(),
|
||||
Err(mpsc::TryRecvError::Empty)
|
||||
));
|
||||
assert_eq!(*requests.latest.lock().unwrap(), "third");
|
||||
drop(requests);
|
||||
assert!(matches!(receiver.recv(), Err(mpsc::RecvError)));
|
||||
@@ -1285,7 +1430,9 @@ mod tests {
|
||||
let strip = Rect::new(origin + 24.0, 64.0, 1552.0, 132.0);
|
||||
for count in [1, 3, 8] {
|
||||
let start = centered_row_start(strip, count, 160.0);
|
||||
let end = start + count as f64 * 160.0 + count.saturating_sub(1) as f64 * WORKSPACE_CARD_GAP;
|
||||
let end = start
|
||||
+ count as f64 * 160.0
|
||||
+ count.saturating_sub(1) as f64 * WORKSPACE_CARD_GAP;
|
||||
assert!((start - strip.x - (strip.x + strip.width - end)).abs() < 0.001);
|
||||
}
|
||||
}
|
||||
@@ -1302,7 +1449,11 @@ mod tests {
|
||||
assert!(strip.height < bounds.height / 2.0);
|
||||
assert!(area.y > strip.y + strip.height);
|
||||
let windows = [(WindowId(1), Rect::new(0.0, 0.0, 800.0, 900.0))];
|
||||
assert!(overview_layout(area, &windows).values().all(|rect| rect.y >= area.y));
|
||||
assert!(
|
||||
overview_layout(area, &windows)
|
||||
.values()
|
||||
.all(|rect| rect.y >= area.y)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1386,7 +1537,11 @@ mod tests {
|
||||
|
||||
let mut reference = AnimatedRect::new(normal);
|
||||
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);
|
||||
|
||||
overview.advance(Duration::from_millis(100), spring, true);
|
||||
@@ -1467,7 +1622,10 @@ mod tests {
|
||||
#[test]
|
||||
fn overview_grid_preserves_preview_aspect_ratio() {
|
||||
let source = Rect::new(0.0, 0.0, 1200.0, 800.0);
|
||||
let layout = overview_layout(Rect::new(0.0, 0.0, 1920.0, 1080.0), &[(WindowId(1), source)]);
|
||||
let layout = overview_layout(
|
||||
Rect::new(0.0, 0.0, 1920.0, 1080.0),
|
||||
&[(WindowId(1), source)],
|
||||
);
|
||||
let preview = layout[&WindowId(1)];
|
||||
|
||||
assert!((preview.width / preview.height - source.width / source.height).abs() < 0.001);
|
||||
@@ -1579,7 +1737,10 @@ mod tests {
|
||||
let mut overview = OverviewState::default();
|
||||
|
||||
overview.enter(
|
||||
HashMap::from([(left, (left_rect, left_rect)), (right, (right_rect, right_rect))]),
|
||||
HashMap::from([
|
||||
(left, (left_rect, left_rect)),
|
||||
(right, (right_rect, right_rect)),
|
||||
]),
|
||||
Some(left),
|
||||
false,
|
||||
);
|
||||
|
||||
@@ -103,21 +103,31 @@ impl PortalSession {
|
||||
|
||||
pub(crate) fn remove(&mut self, owner: u64) {
|
||||
self.waiters.remove(&owner);
|
||||
if self.phase == 2 && self.query.as_ref().is_some_and(|query| query.owner == Some(owner)) {
|
||||
if self.phase == 2
|
||||
&& self
|
||||
.query
|
||||
.as_ref()
|
||||
.is_some_and(|query| query.owner == Some(owner))
|
||||
{
|
||||
self.set_phase(1);
|
||||
}
|
||||
if self.inhibitors.remove(&owner).is_some() {
|
||||
self.inhibitor_revision = self.inhibitor_revision.wrapping_add(1);
|
||||
}
|
||||
let monitor_removed = self.monitors.remove(&owner);
|
||||
let acknowledged = self.query.as_mut().is_some_and(|query| query.pending.remove(&owner));
|
||||
let acknowledged = self
|
||||
.query
|
||||
.as_mut()
|
||||
.is_some_and(|query| query.pending.remove(&owner));
|
||||
if monitor_removed || acknowledged {
|
||||
self.changed();
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -141,7 +151,11 @@ impl PortalSession {
|
||||
}
|
||||
|
||||
pub(crate) fn cancel_query(&mut self, token: u32) {
|
||||
if self.query.as_ref().is_some_and(|query| query.token == token) {
|
||||
if self
|
||||
.query
|
||||
.as_ref()
|
||||
.is_some_and(|query| query.token == token)
|
||||
{
|
||||
self.set_phase(1);
|
||||
}
|
||||
}
|
||||
@@ -154,19 +168,37 @@ impl PortalSession {
|
||||
.is_some_and(|query| query.pending.is_empty() || Instant::now() >= query.deadline)
|
||||
}
|
||||
|
||||
pub(crate) fn validate_end(&self, token: u32, inhibitor_revision: u32, force: bool) -> Result<(), String> {
|
||||
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) {
|
||||
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
|
||||
|| (!force && self.inhibitors.values().any(|inhibitor| inhibitor.flags & 1 != 0))
|
||||
|| (!force
|
||||
&& self
|
||||
.inhibitors
|
||||
.values()
|
||||
.any(|inhibitor| inhibitor.flags & 1 != 0))
|
||||
{
|
||||
return Err("Applications changed their inhibitors; review the confirmation again".into());
|
||||
return Err(
|
||||
"Applications changed their inhibitors; review the confirmation again".into(),
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn commit_end(&mut self, token: u32, inhibitor_revision: u32, force: bool) -> Result<(), String> {
|
||||
pub(crate) fn commit_end(
|
||||
&mut self,
|
||||
token: u32,
|
||||
inhibitor_revision: u32,
|
||||
force: bool,
|
||||
) -> Result<(), String> {
|
||||
self.validate_end(token, inhibitor_revision, force)?;
|
||||
self.set_phase(3);
|
||||
Ok(())
|
||||
@@ -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 {
|
||||
let _ = response.try_send(Response::success(id, self.snapshot()));
|
||||
} else {
|
||||
@@ -224,7 +262,10 @@ impl PortalSession {
|
||||
.map(|inhibitor| json!({"flags": inhibitor.flags, "app": inhibitor.app, "reason": inhibitor.reason}))
|
||||
.collect::<Vec<_>>();
|
||||
let remaining = self.query.as_ref().map_or(0, |query| {
|
||||
query.deadline.saturating_duration_since(Instant::now()).as_millis() as u64
|
||||
query
|
||||
.deadline
|
||||
.saturating_duration_since(Instant::now())
|
||||
.as_millis() as u64
|
||||
});
|
||||
json!({"screensaver-active": self.locked, "session-state": self.phase, "revision": self.revision,
|
||||
"inhibitors": inhibitors, "inhibitor-revision": self.inhibitor_revision,
|
||||
@@ -236,16 +277,17 @@ impl PortalSession {
|
||||
impl crate::Ferese {
|
||||
pub(crate) fn end_portal_session(&mut self) {
|
||||
let token = self.portal_session.query.as_ref().map(|query| query.token);
|
||||
let result =
|
||||
self.loop_handle
|
||||
.insert_source(Timer::from_duration(Duration::from_millis(200)), move |_, _, state| {
|
||||
let result = self.loop_handle.insert_source(
|
||||
Timer::from_duration(Duration::from_millis(200)),
|
||||
move |_, _, state| {
|
||||
if state.portal_session.phase == 3
|
||||
&& state.portal_session.query.as_ref().map(|query| query.token) == token
|
||||
{
|
||||
state.loop_signal.stop();
|
||||
}
|
||||
TimeoutAction::Drop
|
||||
});
|
||||
},
|
||||
);
|
||||
if let Err(error) = result {
|
||||
tracing::error!(%error, "failed to schedule session ending");
|
||||
self.loop_signal.stop();
|
||||
@@ -304,10 +346,20 @@ mod tests {
|
||||
let approved = session.inhibitor_revision;
|
||||
session.register(1, inhibition(1)).unwrap();
|
||||
assert!(session.commit_end(token, approved, true).is_err());
|
||||
assert!(session.commit_end(token, session.inhibitor_revision, false).is_err());
|
||||
session.commit_end(token, session.inhibitor_revision, true).unwrap();
|
||||
assert!(
|
||||
session
|
||||
.commit_end(token, session.inhibitor_revision, false)
|
||||
.is_err()
|
||||
);
|
||||
session
|
||||
.commit_end(token, session.inhibitor_revision, true)
|
||||
.unwrap();
|
||||
assert_eq!(session.snapshot()["session-state"], 3);
|
||||
assert!(session.commit_end(token, session.inhibitor_revision, true).is_err());
|
||||
assert!(
|
||||
session
|
||||
.commit_end(token, session.inhibitor_revision, true)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -350,7 +402,10 @@ mod tests {
|
||||
let response = receiver.try_recv().unwrap();
|
||||
assert_eq!(response.result.unwrap()["screensaver-active"], true);
|
||||
session.set_locked(false);
|
||||
assert_eq!(session.snapshot()["transitions"].as_array().unwrap().len(), 2);
|
||||
assert_eq!(
|
||||
session.snapshot()["transitions"].as_array().unwrap().len(),
|
||||
2
|
||||
);
|
||||
let (sender, receiver) = std::sync::mpsc::sync_channel(1);
|
||||
session.watch(1, 11, session.revision, sender);
|
||||
session.remove(1);
|
||||
|
||||
@@ -26,7 +26,9 @@ struct Registration {
|
||||
|
||||
impl Registration {
|
||||
fn timestamp(&self) -> u64 {
|
||||
self.latest.0.saturating_add(self.latest.1.elapsed().as_millis() as u64)
|
||||
self.latest
|
||||
.0
|
||||
.saturating_add(self.latest.1.elapsed().as_millis() as u64)
|
||||
}
|
||||
|
||||
fn release_all(&mut self) {
|
||||
@@ -53,7 +55,11 @@ impl PortalShortcuts {
|
||||
return Err("Shortcut limit reached".into());
|
||||
}
|
||||
let keymap = crate::config::physical_keymap(input).map_err(|error| error.to_string())?;
|
||||
if self.0.get(&owner).is_some_and(|registration| registration.closed) {
|
||||
if self
|
||||
.0
|
||||
.get(&owner)
|
||||
.is_some_and(|registration| registration.closed)
|
||||
{
|
||||
return Err("Shortcut session has ended".into());
|
||||
}
|
||||
let mut parsed: Vec<(Shortcut, Binding)> = Vec::new();
|
||||
@@ -62,11 +68,17 @@ impl PortalShortcuts {
|
||||
.trigger
|
||||
.split('+')
|
||||
.any(|part| part.trim().eq_ignore_ascii_case("num"));
|
||||
if shortcut.id.is_empty() || shortcut.id.len() > 256 || shortcut.id.chars().any(char::is_control) {
|
||||
if shortcut.id.is_empty()
|
||||
|| shortcut.id.len() > 256
|
||||
|| shortcut.id.chars().any(char::is_control)
|
||||
{
|
||||
return Err("Invalid shortcut identifier".into());
|
||||
}
|
||||
let binding = Binding::portal_trigger(&shortcut.trigger).map_err(|error| error.to_string())?;
|
||||
if configured.iter().any(|other| binding.conflicts(other, &keymap))
|
||||
let binding =
|
||||
Binding::portal_trigger(&shortcut.trigger).map_err(|error| error.to_string())?;
|
||||
if configured
|
||||
.iter()
|
||||
.any(|other| binding.conflicts(other, &keymap))
|
||||
|| parsed
|
||||
.iter()
|
||||
.any(|(other, key)| other.id == shortcut.id || binding.conflicts(key, &keymap))
|
||||
@@ -106,9 +118,9 @@ impl PortalShortcuts {
|
||||
return;
|
||||
};
|
||||
for registration in self.0.values_mut() {
|
||||
registration
|
||||
.shortcuts
|
||||
.retain(|(_, binding)| !configured.iter().any(|key| binding.conflicts(key, &keymap)));
|
||||
registration.shortcuts.retain(|(_, binding)| {
|
||||
!configured.iter().any(|key| binding.conflicts(key, &keymap))
|
||||
});
|
||||
let active_ids = registration
|
||||
.shortcuts
|
||||
.iter()
|
||||
@@ -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() {
|
||||
let Some((shortcut, _)) = registration.shortcuts.iter().find(|(shortcut, binding)| {
|
||||
(!shortcut.num_required || modifiers.num_lock)
|
||||
@@ -225,7 +243,10 @@ mod tests {
|
||||
.register(1, vec![shortcut("one", "Super+q")], &configured, &input)
|
||||
.is_err()
|
||||
);
|
||||
assert_eq!(shortcuts.poll(1, false)["shortcuts"][0]["trigger"], "Ctrl+Alt+a");
|
||||
assert_eq!(
|
||||
shortcuts.poll(1, false)["shortcuts"][0]["trigger"],
|
||||
"Ctrl+Alt+a"
|
||||
);
|
||||
shortcuts.remove(1);
|
||||
assert_eq!(shortcuts.poll(1, false)["closed"], true);
|
||||
shortcuts
|
||||
@@ -274,7 +295,12 @@ mod tests {
|
||||
.register(1, vec![shortcut("one", "Ctrl+Alt+a")], &[], &input)
|
||||
.unwrap();
|
||||
shortcuts.reconcile(&[Binding::portal_trigger("Ctrl+Alt+a").unwrap()], &input);
|
||||
assert!(shortcuts.poll(1, false)["shortcuts"].as_array().unwrap().is_empty());
|
||||
assert!(
|
||||
shortcuts.poll(1, false)["shortcuts"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.is_empty()
|
||||
);
|
||||
assert!(Binding::portal_trigger("Ctrl+Alt+Escape").is_err());
|
||||
assert!(Binding::portal_trigger("Ctrl+Alt+F3").is_err());
|
||||
}
|
||||
@@ -305,8 +331,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn physical_conflicts_include_secondary_layouts_and_num_modifier_is_honored() {
|
||||
let config =
|
||||
crate::config::Config::parse_source("binding keys=\"Ctrl+AD01\" match=\"physical\" action=\"none\"\n")
|
||||
let config = crate::config::Config::parse_source(
|
||||
"binding keys=\"Ctrl+AD01\" match=\"physical\" action=\"none\"\n",
|
||||
)
|
||||
.unwrap();
|
||||
let mut input = config.input_settings().unwrap();
|
||||
input.xkb_layout = "us,fr".into();
|
||||
|
||||
+105
-26
@@ -5,7 +5,9 @@ use std::time::{Duration, Instant};
|
||||
use ferese_animation::AnimatedRect;
|
||||
|
||||
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::desktop::utils::OutputPresentationFeedback;
|
||||
use smithay::output::Output;
|
||||
@@ -52,7 +54,8 @@ impl WindowPresentation {
|
||||
}
|
||||
|
||||
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.emphasis.set_target(0.0);
|
||||
self.shadow.set_target(0.0);
|
||||
@@ -64,21 +67,32 @@ impl WindowPresentation {
|
||||
/// always follows the window's shared rounded geometry.
|
||||
pub fn shadow_factors(self) -> (f64, f64, f64) {
|
||||
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.
|
||||
// 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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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 {
|
||||
stiffness: base.stiffness / (response * response),
|
||||
damping: base.damping / response,
|
||||
@@ -95,11 +109,14 @@ pub(crate) fn take_output_feedback(
|
||||
flags: wp_presentation_feedback::Kind,
|
||||
) -> OutputPresentationFeedback {
|
||||
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 visible_output =
|
||||
|surface: &WlSurface, _: &SurfaceData| rendered_feedback_output(output, surface.into(), rendered);
|
||||
let visible_output = |surface: &WlSurface, _: &SurfaceData| {
|
||||
rendered_feedback_output(output, surface.into(), rendered)
|
||||
};
|
||||
let surface_flags = |surface: &WlSurface, _: &SurfaceData| {
|
||||
flags | surface_presentation_feedback_flags_from_states(surface, None, rendered)
|
||||
};
|
||||
@@ -121,7 +138,11 @@ pub(crate) fn take_output_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
|
||||
.element_render_state(id)
|
||||
.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;
|
||||
|
||||
/// 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 {
|
||||
return radius;
|
||||
}
|
||||
@@ -175,7 +200,12 @@ impl RoundedRect {
|
||||
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);
|
||||
Self {
|
||||
radius: clamp_radius(radius * scale, rect.size),
|
||||
@@ -271,7 +301,11 @@ impl Element for PhysicalShaderElement {
|
||||
self.geometry
|
||||
}
|
||||
|
||||
fn damage_since(&self, _: Scale<f64>, commit: Option<CommitCounter>) -> DamageSet<i32, Physical> {
|
||||
fn damage_since(
|
||||
&self,
|
||||
_: Scale<f64>,
|
||||
commit: Option<CommitCounter>,
|
||||
) -> DamageSet<i32, Physical> {
|
||||
if commit == Some(self.current_commit()) {
|
||||
DamageSet::default()
|
||||
} else {
|
||||
@@ -333,7 +367,11 @@ impl Element for NativeTextureElement {
|
||||
Kind::Unspecified
|
||||
}
|
||||
|
||||
fn damage_since(&self, _: Scale<f64>, commit: Option<CommitCounter>) -> DamageSet<i32, Physical> {
|
||||
fn damage_since(
|
||||
&self,
|
||||
_: Scale<f64>,
|
||||
commit: Option<CommitCounter>,
|
||||
) -> DamageSet<i32, Physical> {
|
||||
if commit == Some(self.commit) {
|
||||
DamageSet::default()
|
||||
} else {
|
||||
@@ -454,7 +492,8 @@ mod tests {
|
||||
fn layered_responses_preserve_damping_and_settle_independently() {
|
||||
use ferese_animation::{AnimatedValue, SpringConfig};
|
||||
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)] {
|
||||
assert!((damping_ratio(spring) - damping_ratio(base)).abs() < 1e-12);
|
||||
assert_eq!(spring.crossing, base.crossing);
|
||||
@@ -482,7 +521,12 @@ mod tests {
|
||||
#[test]
|
||||
fn rounded_outline_clamps_after_physical_snapping() {
|
||||
// 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.radius, 1.5);
|
||||
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(-1.0, size), 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);
|
||||
}
|
||||
|
||||
@@ -528,7 +577,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
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};
|
||||
|
||||
let output = Output::new(
|
||||
@@ -580,9 +631,24 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn reversing_a_grow_replaces_the_smaller_snapshot() {
|
||||
assert!(!snapshot_covers_source((500, 600).into(), (1000, 600).into(), 1.0, 1.0));
|
||||
assert!(snapshot_covers_source((1000, 600).into(), (500, 600).into(), 1.0, 1.0));
|
||||
assert!(!snapshot_covers_source((1000, 600).into(), (500, 600).into(), 1.0, 1.8));
|
||||
assert!(!snapshot_covers_source(
|
||||
(500, 600).into(),
|
||||
(1000, 600).into(),
|
||||
1.0,
|
||||
1.0
|
||||
));
|
||||
assert!(snapshot_covers_source(
|
||||
(1000, 600).into(),
|
||||
(500, 600).into(),
|
||||
1.0,
|
||||
1.0
|
||||
));
|
||||
assert!(!snapshot_covers_source(
|
||||
(1000, 600).into(),
|
||||
(500, 600).into(),
|
||||
1.0,
|
||||
1.8
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -660,11 +726,13 @@ mod tests {
|
||||
fast.callback_sent(start);
|
||||
fast.callback_sent(start + Duration::from_millis(8));
|
||||
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)
|
||||
);
|
||||
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)
|
||||
);
|
||||
}
|
||||
@@ -686,14 +754,25 @@ mod tests {
|
||||
fn fractional_motion_does_not_quantize_to_logical_pixels() {
|
||||
let rect = ferese_layout::Rect::new(0.3, 0.3, 100.2, 80.2);
|
||||
assert_eq!(physical_rect(rect, (0, 0).into(), 1.8).loc, (1, 1).into());
|
||||
assert_eq!(physical_rect(rect, (0, 0).into(), 1.8).size, (180, 144).into());
|
||||
assert_eq!(
|
||||
physical_rect(rect, (0, 0).into(), 1.8).size,
|
||||
(180, 144).into()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adjacent_frames_share_edges_at_all_output_scales() {
|
||||
for scale in [1.0, 1.25, 1.5, 1.8, 2.0] {
|
||||
let left = physical_rect(ferese_layout::Rect::new(13.3, 0.0, 500.4, 800.0), (0, 0).into(), scale);
|
||||
let right = physical_rect(ferese_layout::Rect::new(513.7, 0.0, 500.4, 800.0), (0, 0).into(), scale);
|
||||
let left = physical_rect(
|
||||
ferese_layout::Rect::new(13.3, 0.0, 500.4, 800.0),
|
||||
(0, 0).into(),
|
||||
scale,
|
||||
);
|
||||
let right = physical_rect(
|
||||
ferese_layout::Rect::new(513.7, 0.0, 500.4, 800.0),
|
||||
(0, 0).into(),
|
||||
scale,
|
||||
);
|
||||
assert_eq!(left.loc.x + left.size.w, right.loc.x);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,8 @@ fn mapped_tree(surface: &WlSurface) -> bool {
|
||||
let mut current = Some(surface.clone());
|
||||
while let Some(surface) = current {
|
||||
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;
|
||||
}
|
||||
@@ -29,9 +30,12 @@ impl Ferese {
|
||||
let mut surfaces = Vec::new();
|
||||
if self.session_lock.active() {
|
||||
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(
|
||||
lock.wl_surface(),
|
||||
|surface, _| {
|
||||
surfaces.push(surface.clone());
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
} else {
|
||||
for window in self.space.elements() {
|
||||
@@ -55,7 +59,12 @@ impl Ferese {
|
||||
let output = self
|
||||
.display_presentation
|
||||
.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();
|
||||
Some((id, output))
|
||||
})
|
||||
|
||||
@@ -34,8 +34,8 @@ pub(crate) fn prepare_command(
|
||||
command: &mut Command,
|
||||
capabilities: ClientCapabilities,
|
||||
) -> io::Result<Vec<UnixStream>> {
|
||||
let split_shell_control =
|
||||
capabilities.contains(ClientCapabilities::EFFECTS) && capabilities.contains(ClientCapabilities::SHELL_CONTROL);
|
||||
let split_shell_control = capabilities.contains(ClientCapabilities::EFFECTS)
|
||||
&& capabilities.contains(ClientCapabilities::SHELL_CONTROL);
|
||||
let primary_capabilities = if split_shell_control {
|
||||
ClientCapabilities::EFFECTS
|
||||
} else {
|
||||
@@ -54,7 +54,10 @@ pub(crate) fn prepare_command(
|
||||
if split_shell_control {
|
||||
let control = insert_private_client(state, ClientCapabilities::SHELL_CONTROL)?;
|
||||
|
||||
command.env("FERESE_SHELL_CONTROL_SOCKET", control.as_raw_fd().to_string());
|
||||
command.env(
|
||||
"FERESE_SHELL_CONTROL_SOCKET",
|
||||
control.as_raw_fd().to_string(),
|
||||
);
|
||||
clients.push(control);
|
||||
}
|
||||
|
||||
@@ -76,7 +79,10 @@ pub(crate) fn prepare_command(
|
||||
Ok(clients)
|
||||
}
|
||||
|
||||
fn insert_private_client(state: &mut Ferese, capabilities: ClientCapabilities) -> io::Result<UnixStream> {
|
||||
fn insert_private_client(
|
||||
state: &mut Ferese,
|
||||
capabilities: ClientCapabilities,
|
||||
) -> io::Result<UnixStream> {
|
||||
let (server, client) = UnixStream::pair()?;
|
||||
|
||||
state.display_handle.insert_client(
|
||||
|
||||
@@ -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 owner = Rc::new(RefCell::new(Some(child)));
|
||||
let watched = owner.clone();
|
||||
let result = pidfd(pid).and_then(|fd| {
|
||||
handle
|
||||
.insert_source(Generic::new(fd, Interest::READ, Mode::Level), move |_, _, _| {
|
||||
.insert_source(
|
||||
Generic::new(fd, Interest::READ, Mode::Level),
|
||||
move |_, _, _| {
|
||||
Ok(if reap(&watched) {
|
||||
PostAction::Remove
|
||||
} else {
|
||||
PostAction::Continue
|
||||
})
|
||||
})
|
||||
},
|
||||
)
|
||||
.map_err(io::Error::other)
|
||||
});
|
||||
|
||||
if let Err(error) = result {
|
||||
warn!(%error, pid, "could not watch client exit with pidfd; polling instead");
|
||||
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(
|
||||
Timer::from_duration(Duration::from_millis(250)),
|
||||
move |_, _, _| {
|
||||
if reap(&watched) {
|
||||
TimeoutAction::Drop
|
||||
} else {
|
||||
TimeoutAction::ToDuration(Duration::from_millis(250))
|
||||
}
|
||||
}) {
|
||||
},
|
||||
) {
|
||||
warn!(%error, pid, "could not schedule client exit polling; stopping child");
|
||||
if let Some(mut child) = owner.borrow_mut().take() {
|
||||
terminate_child(&mut child);
|
||||
|
||||
@@ -51,13 +51,16 @@ impl Supervisor {
|
||||
let watched = pidfd(pid).and_then(|fd| {
|
||||
state
|
||||
.loop_handle
|
||||
.insert_source(Generic::new(fd, Interest::READ, Mode::Level), move |_, _, state| {
|
||||
.insert_source(
|
||||
Generic::new(fd, Interest::READ, Mode::Level),
|
||||
move |_, _, state| {
|
||||
if let Some(owner) = weak.upgrade() {
|
||||
owner.borrow_mut().source = None;
|
||||
Self::exited(&owner, state);
|
||||
}
|
||||
Ok(PostAction::Remove)
|
||||
})
|
||||
},
|
||||
)
|
||||
.map_err(io::Error::other)
|
||||
});
|
||||
if let Ok(token) = watched {
|
||||
@@ -69,7 +72,9 @@ impl Supervisor {
|
||||
let weak = Rc::downgrade(owner);
|
||||
let token = state
|
||||
.loop_handle
|
||||
.insert_source(Timer::from_duration(Duration::from_millis(250)), move |_, _, state| {
|
||||
.insert_source(
|
||||
Timer::from_duration(Duration::from_millis(250)),
|
||||
move |_, _, state| {
|
||||
let Some(owner) = weak.upgrade() else {
|
||||
return TimeoutAction::Drop;
|
||||
};
|
||||
@@ -85,7 +90,8 @@ impl Supervisor {
|
||||
} else {
|
||||
TimeoutAction::ToDuration(Duration::from_millis(250))
|
||||
}
|
||||
})
|
||||
},
|
||||
)
|
||||
.expect("register desktop child watcher");
|
||||
owner.borrow_mut().source = Some(token);
|
||||
} else {
|
||||
@@ -177,7 +183,8 @@ mod tests {
|
||||
if !crate::startup_tests::private_runtime(test) {
|
||||
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 state = crate::startup_tests::state(&mut events);
|
||||
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 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 rows: Vec<_> = text.lines().map(str::to_owned).collect();
|
||||
if rows.len() == 2 {
|
||||
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[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();
|
||||
unsafe {
|
||||
libc::kill(child_pid as i32, libc::SIGKILL);
|
||||
}
|
||||
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!(owner.borrow().source.is_some(), "restart timer armed");
|
||||
owner.borrow_mut().stop(&state.loop_handle);
|
||||
let stopped = Instant::now();
|
||||
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!(owner.borrow().source.is_none());
|
||||
@@ -265,7 +284,9 @@ time.sleep(60)
|
||||
break pid;
|
||||
}
|
||||
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);
|
||||
let status_path = format!("/proc/{pid}/status");
|
||||
@@ -279,7 +300,10 @@ time.sleep(60)
|
||||
if !running {
|
||||
break;
|
||||
}
|
||||
assert!(Instant::now() < deadline, "helper descendant survived shutdown");
|
||||
assert!(
|
||||
Instant::now() < deadline,
|
||||
"helper descendant survived shutdown"
|
||||
);
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
assert!(owner.borrow().child.is_none());
|
||||
|
||||
+87
-29
@@ -14,14 +14,19 @@ impl Prepared {
|
||||
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 {
|
||||
return Err("configuration exceeds the 60 KiB live-reload limit".into());
|
||||
}
|
||||
|
||||
let document = ferese_config::Document::parse(&source).map_err(|e| e.to_string())?;
|
||||
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(
|
||||
[&policy.file, &policy.light.file, &policy.dark.file]
|
||||
@@ -76,8 +81,11 @@ impl Worker {
|
||||
event_loop: &mut calloop::EventLoop<'static, crate::Ferese>,
|
||||
) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let (requests, receiver) = std::sync::mpsc::sync_channel::<Request>(8);
|
||||
let (results, events) = calloop::channel::channel::<(Result<Prepared, String>, Vec<PathBuf>, Option<Reply>)>();
|
||||
event_loop.handle().insert_source(events, |event, _, state| {
|
||||
let (results, events) =
|
||||
calloop::channel::channel::<(Result<Prepared, String>, Vec<PathBuf>, Option<Reply>)>();
|
||||
event_loop
|
||||
.handle()
|
||||
.insert_source(events, |event, _, state| {
|
||||
if let calloop::channel::Event::Msg((result, watched, reply)) = event {
|
||||
state.theme_engine.watch_files(&watched);
|
||||
let result = result.and_then(|prepared| state.apply_prepared_config(prepared));
|
||||
@@ -110,7 +118,11 @@ impl Worker {
|
||||
let result = path
|
||||
.as_ref()
|
||||
.map_or_else(
|
||||
|| request.fallback.ok_or_else(|| "No configuration available".into()),
|
||||
|| {
|
||||
request
|
||||
.fallback
|
||||
.ok_or_else(|| "No configuration available".into())
|
||||
},
|
||||
|path| {
|
||||
if path.exists() || !request.force {
|
||||
crate::config::read_source(path)
|
||||
@@ -124,7 +136,8 @@ impl Worker {
|
||||
// Auto mode still resolves time and system appearance.
|
||||
if let Some(previous) = &cached
|
||||
&& 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
|
||||
{
|
||||
return Ok(previous.clone());
|
||||
@@ -149,16 +162,23 @@ impl Worker {
|
||||
}
|
||||
|
||||
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 {
|
||||
force,
|
||||
fallback: self.config_source.clone(),
|
||||
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) {
|
||||
let (std::sync::mpsc::TrySendError::Full(request) | std::sync::mpsc::TrySendError::Disconnected(request)) =
|
||||
error;
|
||||
let (std::sync::mpsc::TrySendError::Full(request)
|
||||
| std::sync::mpsc::TrySendError::Disconnected(request)) = error;
|
||||
if let Some((id, reply)) = request.reply {
|
||||
let _ = reply.try_send(ferese_ipc::Response::error(
|
||||
id,
|
||||
@@ -276,8 +296,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
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;
|
||||
}
|
||||
let mut events = calloop::EventLoop::try_new().unwrap();
|
||||
@@ -286,8 +307,12 @@ mod tests {
|
||||
let image_path = directory.path().join("wallpaper.png");
|
||||
image::RgbaImage::new(2, 2).save(&image_path).unwrap();
|
||||
for mode in ["fit", "fill"] {
|
||||
let source = format!("wallpaper {{ path \"{}\"; mode \"{mode}\"; }}", image_path.display());
|
||||
let (_, runtime, candidate) = crate::config::prepare(&source, directory.path()).unwrap();
|
||||
let source = format!(
|
||||
"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.
|
||||
state.theme_engine.live.theme = candidate.theme.clone();
|
||||
state.theme_engine.live.presented = candidate.theme;
|
||||
@@ -310,8 +335,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
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;
|
||||
}
|
||||
let mut events = calloop::EventLoop::try_new().unwrap();
|
||||
@@ -326,7 +352,9 @@ mod tests {
|
||||
// Exercise application failure after preparation, for example when an
|
||||
// XKB file disappears before the compositor installs the new keymap.
|
||||
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_eq!(state.xwayland_config.enabled, before_xwayland);
|
||||
assert_eq!(state.activities.snapshot().revision, before_activities);
|
||||
@@ -336,9 +364,14 @@ mod tests {
|
||||
let mut runtime = crate::config::Config::default().runtime_config().unwrap();
|
||||
runtime.xwayland.enabled = !before_xwayland;
|
||||
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.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);
|
||||
}
|
||||
|
||||
@@ -379,7 +412,12 @@ mod tests {
|
||||
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
|
||||
let valid = "animations { speed 0.5; }";
|
||||
std::fs::write(&path, valid).unwrap();
|
||||
for (index, source) in [Some(valid), Some(valid), Some("animations { speed -1.0; }"), None]
|
||||
for (index, source) in [
|
||||
Some(valid),
|
||||
Some(valid),
|
||||
Some("animations { speed -1.0; }"),
|
||||
None,
|
||||
]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
@@ -392,7 +430,9 @@ mod tests {
|
||||
let before = state.ipc_events.config_revision;
|
||||
let theme_before = state.theme_engine.live.theme.clone();
|
||||
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 {
|
||||
// Present a frame while the duplicate reload is queued. Theme
|
||||
// revisions include these frames; config revisions do not.
|
||||
@@ -401,7 +441,9 @@ mod tests {
|
||||
}
|
||||
let deadline = Instant::now() + Duration::from_secs(5);
|
||||
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() {
|
||||
break response;
|
||||
}
|
||||
@@ -415,7 +457,10 @@ mod tests {
|
||||
);
|
||||
assert_eq!(state.config_source.as_deref(), Some(valid));
|
||||
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 {
|
||||
assert_eq!(state.theme_engine.live.theme, theme_before);
|
||||
}
|
||||
@@ -460,8 +505,9 @@ mod tests {
|
||||
state.theme_engine.live.presented = candidate.theme;
|
||||
|
||||
for inactive_opacity in [1.0, 0.75, 0.9] {
|
||||
let source =
|
||||
format!("appearance {{ focus-effect {{ inactive-opacity {inactive_opacity}; inactive-dim 0.25; }}; }}");
|
||||
let source = format!(
|
||||
"appearance {{ focus-effect {{ inactive-opacity {inactive_opacity}; inactive-dim 0.25; }}; }}"
|
||||
);
|
||||
state.reload_config_source(source).unwrap();
|
||||
assert_eq!(state.focus_effect.inactive_opacity, inactive_opacity);
|
||||
let (alpha, dim) = state.focus_effect.resolve(0.0, true, 0.0);
|
||||
@@ -486,7 +532,10 @@ mod tests {
|
||||
);
|
||||
assert_eq!(state.focus_effect, previous);
|
||||
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();
|
||||
state.poll_theme();
|
||||
assert_eq!(state.focus_effect.inactive_dim, 0.0);
|
||||
@@ -514,12 +563,17 @@ mod tests {
|
||||
.env_remove("FERESE_SOCKET")
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(result.status.success(), "{}", String::from_utf8_lossy(&result.stderr));
|
||||
assert!(
|
||||
result.status.success(),
|
||||
"{}",
|
||||
String::from_utf8_lossy(&result.stderr)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
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 display = smithay::reexports::wayland_server::Display::new().unwrap();
|
||||
let mut state = crate::Ferese::new(&mut event_loop, display, runtime).unwrap();
|
||||
@@ -563,7 +617,11 @@ mod tests {
|
||||
.env_remove("FERESE_SOCKET")
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(result.status.success(), "{}", String::from_utf8_lossy(&result.stderr));
|
||||
assert!(
|
||||
result.status.success(),
|
||||
"{}",
|
||||
String::from_utf8_lossy(&result.stderr)
|
||||
);
|
||||
return;
|
||||
}
|
||||
let directory = Directory::new();
|
||||
|
||||
@@ -39,7 +39,10 @@ impl Backdrop {
|
||||
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| {
|
||||
source.geometry != geometry
|
||||
|| source.alpha != element.alpha()
|
||||
@@ -48,7 +51,10 @@ impl Backdrop {
|
||||
|| source.visible != visible
|
||||
});
|
||||
if let Some(previous) = previous {
|
||||
changed |= element.damage_since(scale, Some(previous.commit)).iter().any(|damage| {
|
||||
changed |= element
|
||||
.damage_since(scale, Some(previous.commit))
|
||||
.iter()
|
||||
.any(|damage| {
|
||||
let damage = Rectangle::new(damage.loc + geometry.loc, damage.size);
|
||||
visible.iter().any(|rect| rect.overlaps(damage))
|
||||
});
|
||||
@@ -163,7 +169,9 @@ mod tests {
|
||||
}
|
||||
fn current_commit(&self) -> CommitCounter {
|
||||
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
|
||||
}
|
||||
@@ -173,7 +181,11 @@ mod tests {
|
||||
fn geometry(&self, _: RenderScale<f64>) -> Rectangle<i32, Physical> {
|
||||
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) {
|
||||
DamageSet::default()
|
||||
} else {
|
||||
@@ -209,13 +221,31 @@ mod tests {
|
||||
|
||||
// Open: ignore a repeated instance of the cached popup material, and
|
||||
// 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!(!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.
|
||||
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(
|
||||
&backdrop,
|
||||
&elements,
|
||||
@@ -225,10 +255,28 @@ mod tests {
|
||||
));
|
||||
|
||||
// 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!(update_shared(&backdrop, &elements, &owner, region, 1.0.into()));
|
||||
assert!(!update_shared(&backdrop, &elements, &owner, region, 1.0.into()));
|
||||
assert!(update_shared(
|
||||
&backdrop,
|
||||
&elements,
|
||||
&owner,
|
||||
region,
|
||||
1.0.into()
|
||||
));
|
||||
assert!(!update_shared(
|
||||
&backdrop,
|
||||
&elements,
|
||||
&owner,
|
||||
region,
|
||||
1.0.into()
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -33,10 +33,14 @@ fn render_window_texture(
|
||||
if bytes == 0 || bytes > remaining {
|
||||
return Ok(None);
|
||||
}
|
||||
let content = render_elements_from_surface_tree::<GlesRenderer, WaylandSurfaceRenderElement<GlesRenderer>>(
|
||||
let content = render_elements_from_surface_tree::<
|
||||
GlesRenderer,
|
||||
WaylandSurfaceRenderElement<GlesRenderer>,
|
||||
>(
|
||||
renderer,
|
||||
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,
|
||||
1.0,
|
||||
RenderElementKind::Unspecified,
|
||||
@@ -46,7 +50,11 @@ fn render_window_texture(
|
||||
}
|
||||
let mut texture = match reusable.filter(|texture| texture.size() == (size.w, size.h).into()) {
|
||||
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)?;
|
||||
@@ -93,13 +101,22 @@ pub(crate) fn capture_window_frame(
|
||||
.get_or_insert_threadsafe(|| Arc::new(CaptureTarget::default()))
|
||||
.clone();
|
||||
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(
|
||||
renderer,
|
||||
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();
|
||||
if let Some((state, cursor_geometry)) = cursor {
|
||||
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
|
||||
.render(&mut target, (size.w, size.h).into(), Transform::Normal)
|
||||
.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 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())?;
|
||||
if mapping.format() != Some(Fourcc::Abgr8888) {
|
||||
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
|
||||
// needs a retained mapping plus an EGL-context-owned fence, polled before
|
||||
// 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 {
|
||||
return Err("Incomplete window readback".into());
|
||||
}
|
||||
@@ -164,7 +187,9 @@ mod tests {
|
||||
convert_window_pixels(&mut pixels);
|
||||
assert_eq!(
|
||||
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
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,11 @@ pub(crate) struct ClosedWindow {
|
||||
|
||||
impl ClosedWindow {
|
||||
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 {
|
||||
@@ -82,7 +86,9 @@ pub(super) fn grouped_elements(
|
||||
.render
|
||||
.closing
|
||||
.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()
|
||||
.collect();
|
||||
if windows.is_empty() {
|
||||
@@ -97,7 +103,8 @@ pub(super) fn grouped_elements(
|
||||
window.advance(delta, state.spring_config);
|
||||
}
|
||||
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 Some(programs) = corner_program(&mut state.render, renderer, window.shape) else {
|
||||
continue;
|
||||
@@ -124,7 +131,8 @@ pub(super) fn grouped_elements(
|
||||
constrain,
|
||||
corners,
|
||||
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_gradient,
|
||||
window.presentation.focus() as f32,
|
||||
@@ -196,8 +204,13 @@ pub(super) fn grouped_elements(
|
||||
elements.push(material);
|
||||
}
|
||||
if window.material.is_none()
|
||||
&& let Some(blur) =
|
||||
super::window_blur::window_blur_element(state, renderer, output, window.presentation, corners)
|
||||
&& let Some(blur) = super::window_blur::window_blur_element(
|
||||
state,
|
||||
renderer,
|
||||
output,
|
||||
window.presentation,
|
||||
corners,
|
||||
)
|
||||
{
|
||||
elements.push(blur);
|
||||
}
|
||||
|
||||
@@ -219,7 +219,9 @@ fn window_corner_shader_timing_sample() {
|
||||
Uniform::new("clip_rect", [0.0f32, 0.0, 1024.0, 768.0]),
|
||||
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] {
|
||||
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 {
|
||||
let started = std::time::Instant::now();
|
||||
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
|
||||
.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 mut texture: GlesTexture = renderer.create_buffer(Fourcc::Abgr8888, size).unwrap();
|
||||
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;
|
||||
let mut renderer = renderer();
|
||||
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 groups = [
|
||||
GroupBounds {
|
||||
@@ -346,7 +355,19 @@ fn panel_shader_matches_core_outlines_and_resets_for_popovers() {
|
||||
let matrices = scene
|
||||
.packets()
|
||||
.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<_>>();
|
||||
let mut uniforms = vec![
|
||||
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 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;
|
||||
assert!(
|
||||
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() {
|
||||
let mut renderer = renderer();
|
||||
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] {
|
||||
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(
|
||||
&mut renderer,
|
||||
&material.0,
|
||||
@@ -537,7 +564,11 @@ fn docked_panel_fill_covers_the_entire_attached_edge() {
|
||||
"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() {
|
||||
let mut renderer = renderer();
|
||||
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 radii = [0.0f32, 4.0, 12.0, 20.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 resources = RenderResources::default();
|
||||
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 radius = 12.375f32;
|
||||
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 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!(
|
||||
actual.abs_diff((expected * 255.0).round() as u8) <= 2,
|
||||
"outline mismatch at ({x}, {y})"
|
||||
@@ -680,8 +717,16 @@ fn window_role_change_invalidates_decorations_and_preserves_the_resize_snapshot(
|
||||
.unwrap()
|
||||
.borders
|
||||
.contexts
|
||||
.insert(context.clone(), CachedBorder { element, parameters });
|
||||
let texture = renderer.create_buffer(Fourcc::Abgr8888, (64, 64).into()).unwrap();
|
||||
.insert(
|
||||
context.clone(),
|
||||
CachedBorder {
|
||||
element,
|
||||
parameters,
|
||||
},
|
||||
);
|
||||
let texture = renderer
|
||||
.create_buffer(Fourcc::Abgr8888, (64, 64).into())
|
||||
.unwrap();
|
||||
resources.set_snapshot(
|
||||
id,
|
||||
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) {
|
||||
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][1] - 2.0 * points[2][1] + points[1][1]),
|
||||
@@ -719,7 +767,10 @@ fn endpoint_derivatives(points: Cubic, end: bool) -> (Point2, Point2) {
|
||||
)
|
||||
} 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][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 {
|
||||
(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]
|
||||
@@ -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");
|
||||
let (before, before_acceleration) = endpoint_derivatives(pair[0], true);
|
||||
let (after, after_acceleration) = endpoint_derivatives(pair[1], false);
|
||||
let cosine =
|
||||
(before[0] * after[0] + before[1] * after[1]) / before[0].hypot(before[1]) / after[0].hypot(after[1]);
|
||||
let cosine = (before[0] * after[0] + before[1] * after[1])
|
||||
/ before[0].hypot(before[1])
|
||||
/ after[0].hypot(after[1]);
|
||||
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
|
||||
// curve. Preserve and document its limitation instead of falsely
|
||||
// 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 size = (1024, 768).into();
|
||||
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)); }";
|
||||
for (name, source) in reference
|
||||
.as_deref()
|
||||
@@ -784,7 +840,9 @@ fn corner_heavy_shader_timing_sample() {
|
||||
for i in 0..110 {
|
||||
let start = std::time::Instant::now();
|
||||
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
|
||||
.render_pixel_shader_to(
|
||||
@@ -817,7 +875,9 @@ fn window_tint_does_not_inherit_panel_geometry() {
|
||||
use smithay::backend::renderer::gles::UniformValue;
|
||||
use smithay::output::{Mode, PhysicalProperties, Subpixel};
|
||||
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 rect = Rectangle::from_size((96, 96).into());
|
||||
let mut frame = renderer
|
||||
@@ -837,7 +897,11 @@ fn window_tint_does_not_inherit_panel_geometry() {
|
||||
.unwrap();
|
||||
let _ = frame.finish().unwrap();
|
||||
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();
|
||||
renderer.map_texture(&mapping).unwrap().to_vec()
|
||||
}
|
||||
@@ -863,7 +927,8 @@ fn window_tint_does_not_inherit_panel_geometry() {
|
||||
Some((0, 0).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] {
|
||||
let tint = window_tint_element(
|
||||
&mut resources,
|
||||
@@ -949,14 +1014,23 @@ fn floating_blur_sampling_halo_does_not_paint_neighboring_islands() {
|
||||
];
|
||||
let rect = Rectangle::<i32, Physical>::new(
|
||||
(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];
|
||||
for p in pixels.chunks_exact_mut(4) {
|
||||
p[3] = 255;
|
||||
}
|
||||
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();
|
||||
let uniforms = vec![
|
||||
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())
|
||||
.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();
|
||||
// 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
|
||||
.render_texture_from_to(
|
||||
&source,
|
||||
@@ -1000,7 +1079,11 @@ fn floating_blur_sampling_halo_does_not_paint_neighboring_islands() {
|
||||
.unwrap();
|
||||
let _ = frame.finish().unwrap();
|
||||
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();
|
||||
let rendered = renderer.map_texture(&mapping).unwrap().to_vec();
|
||||
let mut painted = 0;
|
||||
|
||||
@@ -13,10 +13,14 @@ pub(super) fn corner_program(
|
||||
shape: CornerShape,
|
||||
) -> Option<RoundedClipPrograms> {
|
||||
let context = renderer.context_id().erased();
|
||||
if let Some(program) = resources.contexts.get(&context).and_then(|programs| match shape {
|
||||
if let Some(program) = resources
|
||||
.contexts
|
||||
.get(&context)
|
||||
.and_then(|programs| match shape {
|
||||
CornerShape::Circular => programs.rounded.as_ref(),
|
||||
CornerShape::Continuous => programs.continuous.as_ref(),
|
||||
}) {
|
||||
})
|
||||
{
|
||||
return Some(program.clone());
|
||||
}
|
||||
|
||||
@@ -43,11 +47,18 @@ pub(super) fn corner_program(
|
||||
UniformName::new("opacity", UniformType::_1f),
|
||||
UniformName::new("shadow_color", UniformType::_4f),
|
||||
];
|
||||
let texture =
|
||||
renderer.compile_custom_texture_shader(corner_shader_for(ROUNDED_TEXTURE_SHADER, shape), &texture_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 texture = renderer.compile_custom_texture_shader(
|
||||
corner_shader_for(ROUNDED_TEXTURE_SHADER, shape),
|
||||
&texture_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(
|
||||
corner_shader_for(ROUNDED_SOLID_SHADER, shape),
|
||||
&[
|
||||
@@ -57,7 +68,9 @@ pub(super) fn corner_program(
|
||||
],
|
||||
);
|
||||
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 {
|
||||
Ok((texture, border, shadow, solid)) => {
|
||||
@@ -130,13 +143,22 @@ pub(super) fn window_border_element(
|
||||
Some(g) => (
|
||||
g.from.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]),
|
||||
};
|
||||
let parameters = BorderParameters {
|
||||
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,
|
||||
width,
|
||||
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 parameters = BorderParameters {
|
||||
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,
|
||||
width: 0.0,
|
||||
color,
|
||||
@@ -253,7 +279,10 @@ pub(super) fn window_tint_element(
|
||||
} else {
|
||||
&mut resources.windows.entry(id).or_default().dim
|
||||
};
|
||||
let cached = buffers.contexts.entry(context).or_insert_with(|| CachedBorder {
|
||||
let cached = buffers
|
||||
.contexts
|
||||
.entry(context)
|
||||
.or_insert_with(|| CachedBorder {
|
||||
element: SharedPixelShaderElement::new(
|
||||
program.0,
|
||||
geometry,
|
||||
@@ -311,7 +340,11 @@ pub(super) fn window_shadow_element(
|
||||
let parameters = ShadowParameters {
|
||||
blur: (blur * scale) as f32,
|
||||
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,
|
||||
opacity: opacity as f32,
|
||||
color,
|
||||
@@ -349,8 +382,16 @@ pub(super) fn window_shadow_element(
|
||||
Some(PhysicalShaderElement {
|
||||
inner: cached.element.clone(),
|
||||
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 offset_y = offset_y.round() as i32;
|
||||
|
||||
@@ -399,21 +444,30 @@ pub(super) fn rounded_window_elements(
|
||||
let geometry = window.geometry();
|
||||
let mut reference = geometry.to_physical_precise_round(scale);
|
||||
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
|
||||
} else {
|
||||
behavior
|
||||
};
|
||||
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 surface = toplevel.wl_surface();
|
||||
|
||||
let mut content = PopupManager::popups_for_surface(surface)
|
||||
.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,
|
||||
popup.wl_surface(),
|
||||
location + offset,
|
||||
|
||||
@@ -12,8 +12,9 @@ pub(super) fn append_material_surface(
|
||||
output_crop: Rectangle<i32, Physical>,
|
||||
elements: &mut Vec<AnimatedWindowRenderElement>,
|
||||
) {
|
||||
let radius =
|
||||
if crate::effects::surface_role(surface).is_some_and(|(role, _)| role == crate::effects::SemanticRole::Panel) {
|
||||
let radius = if crate::effects::surface_role(surface)
|
||||
.is_some_and(|(role, _)| role == crate::effects::SemanticRole::Panel)
|
||||
{
|
||||
state.theme_settings.panel_radius
|
||||
} else {
|
||||
state.theme_settings.material_radius
|
||||
@@ -21,18 +22,24 @@ pub(super) fn append_material_surface(
|
||||
let regions = crate::effects::surface_regions(surface);
|
||||
let opacities = crate::effects::surface_region_opacities(surface);
|
||||
let targets: Vec<_> = match ®ions {
|
||||
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
|
||||
.iter()
|
||||
.enumerate()
|
||||
.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,
|
||||
opacities.get(index).copied().unwrap_or(1000) as f32 / 1000.0,
|
||||
)
|
||||
})
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
@@ -63,8 +70,12 @@ pub(super) fn append_material_surface(
|
||||
})
|
||||
.collect();
|
||||
if let Some(buffers) = state.render.surfaces.get_mut(surface) {
|
||||
buffers.contexts.retain(|(_, _, index), _| *index < targets.len());
|
||||
buffers.captures.retain(|(_, _, index), _| *index < targets.len());
|
||||
buffers
|
||||
.contexts
|
||||
.retain(|(_, _, index), _| *index < targets.len());
|
||||
buffers
|
||||
.captures
|
||||
.retain(|(_, _, index), _| *index < targets.len());
|
||||
}
|
||||
let material = (!materials.is_empty() && regions.is_none()).then_some(());
|
||||
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.
|
||||
let content = render_elements_from_surface_tree::<GlesRenderer, WaylandSurfaceRenderElement<GlesRenderer>>(
|
||||
let content = render_elements_from_surface_tree::<
|
||||
GlesRenderer,
|
||||
WaylandSurfaceRenderElement<GlesRenderer>,
|
||||
>(
|
||||
renderer,
|
||||
surface,
|
||||
content_origin.to_physical_precise_round(scale),
|
||||
@@ -118,7 +132,11 @@ fn material_region_geometry(
|
||||
scale: f64,
|
||||
) -> Option<(Rectangle<i32, Logical>, RoundedRect)> {
|
||||
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(),
|
||||
)
|
||||
.intersection(geometry.to_f64())?;
|
||||
@@ -135,7 +153,10 @@ fn material_region_geometry(
|
||||
fn framebuffer_corner_radii(radii: [f32; 4], transform: Transform) -> [f32; 4] {
|
||||
let size = Size::<f64, Physical>::from((1., 1.));
|
||||
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 index = match (point.x > 0.5, point.y > 0.5) {
|
||||
(false, false) => 0,
|
||||
@@ -181,7 +202,11 @@ fn resolve_surface_material(
|
||||
panel_opacity: Option<f32>,
|
||||
panel_style: ferese_panel::PanelStyle,
|
||||
) -> 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 {
|
||||
material.opacity = ferese_config::theme::effective_panel_opacity(
|
||||
material.opacity,
|
||||
@@ -226,7 +251,8 @@ pub(super) fn material_element_with_role(
|
||||
} else {
|
||||
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 {
|
||||
state.theme_settings.surface_raised_color
|
||||
} else {
|
||||
@@ -250,8 +276,8 @@ pub(super) fn material_element_with_role(
|
||||
.to_logical(scale)
|
||||
.to_i32_ceil();
|
||||
let touches_top = role == crate::effects::SemanticRole::Panel && geometry.loc.y == 0;
|
||||
let touches_bottom =
|
||||
role == crate::effects::SemanticRole::Panel && geometry.loc.y + geometry.size.h >= output_size.h;
|
||||
let touches_bottom = role == crate::effects::SemanticRole::Panel
|
||||
&& geometry.loc.y + geometry.size.h >= output_size.h;
|
||||
let radii = ferese_panel::CornerRadii(radii)
|
||||
.at_top_edge(touches_top)
|
||||
.at_edge(ferese_panel::Edge::Bottom, touches_bottom)
|
||||
@@ -279,14 +305,21 @@ pub(super) fn material_element_with_role(
|
||||
corners.rect.size,
|
||||
);
|
||||
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_physical = sample_geometry.to_physical_precise_round(scale);
|
||||
let panel = (role == crate::effects::SemanticRole::Panel)
|
||||
.then(|| crate::effects::surface_panel_presentation(surface))
|
||||
.flatten()
|
||||
.map(|presentation| 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;
|
||||
.map(|presentation| {
|
||||
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 {
|
||||
panel,
|
||||
corner_radii,
|
||||
@@ -321,7 +354,11 @@ pub(super) fn material_element_with_role(
|
||||
.get(&capture_key)
|
||||
.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(
|
||||
renderer,
|
||||
Fourcc::Abgr8888,
|
||||
capture_size,
|
||||
)
|
||||
.map_err(|error| {
|
||||
tracing::warn!(%error, ?sample_physical, "backdrop capture allocation failed");
|
||||
error
|
||||
@@ -364,7 +401,9 @@ pub(super) fn material_element_with_role(
|
||||
MaterialElement::Fill(SharedPixelShaderElement::new(
|
||||
program.0.clone(),
|
||||
geometry,
|
||||
parameters.opaque.then(|| vec![Rectangle::from_size(geometry.size)]),
|
||||
parameters
|
||||
.opaque
|
||||
.then(|| vec![Rectangle::from_size(geometry.size)]),
|
||||
1.0,
|
||||
material_uniforms(¶meters),
|
||||
RenderElementKind::Unspecified,
|
||||
@@ -404,13 +443,17 @@ pub(super) fn material_element_with_role(
|
||||
|| cached.parameters.panel != parameters.panel
|
||||
{
|
||||
cached.shadow.resize(parameters.shadow_bounds, None);
|
||||
cached.shadow.update_uniforms(decoration_uniforms(¶meters, 2.0));
|
||||
cached
|
||||
.shadow
|
||||
.update_uniforms(decoration_uniforms(¶meters, 2.0));
|
||||
}
|
||||
match &mut cached.element {
|
||||
MaterialElement::Fill(element) => {
|
||||
element.resize(
|
||||
geometry,
|
||||
parameters.opaque.then(|| vec![Rectangle::from_size(geometry.size)]),
|
||||
parameters
|
||||
.opaque
|
||||
.then(|| vec![Rectangle::from_size(geometry.size)]),
|
||||
);
|
||||
element.update_uniforms(material_uniforms(¶meters));
|
||||
}
|
||||
@@ -421,7 +464,9 @@ pub(super) fn material_element_with_role(
|
||||
let background = match &cached.element {
|
||||
// Keep the shader's opaque regions when its canvas already has the
|
||||
// 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 {
|
||||
inner: element.clone(),
|
||||
geometry: corners.rect,
|
||||
@@ -432,7 +477,11 @@ pub(super) fn material_element_with_role(
|
||||
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 {
|
||||
radius
|
||||
} 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();
|
||||
if let Some(program) = resources
|
||||
.contexts
|
||||
@@ -449,7 +501,8 @@ pub(super) fn blur_program(resources: &mut RenderResources, renderer: &mut GlesR
|
||||
{
|
||||
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) => {
|
||||
let program = BlurProgram(program);
|
||||
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![
|
||||
Uniform::new("visible_rect", p.visible_framebuffer),
|
||||
Uniform::new("material_radii", p.corner_radii),
|
||||
Uniform::new("texture_size", [p.sample_framebuffer[2], p.sample_framebuffer[3]]),
|
||||
Uniform::new("capture_origin", [p.sample_framebuffer[0], p.sample_framebuffer[1]]),
|
||||
Uniform::new(
|
||||
"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("presentation_alpha", p.presentation_alpha),
|
||||
Uniform::new("background_opacity", p.background_opacity),
|
||||
@@ -524,10 +583,14 @@ pub(super) fn material_program_for_corners(
|
||||
shape: CornerShape,
|
||||
) -> Option<MaterialProgram> {
|
||||
let context = renderer.context_id().erased();
|
||||
if let Some(program) = resources.contexts.get(&context).and_then(|programs| match shape {
|
||||
if let Some(program) = resources
|
||||
.contexts
|
||||
.get(&context)
|
||||
.and_then(|programs| match shape {
|
||||
CornerShape::Circular => programs.material.as_ref(),
|
||||
CornerShape::Continuous => programs.window_material.as_ref(),
|
||||
}) {
|
||||
})
|
||||
{
|
||||
return Some(program.clone());
|
||||
}
|
||||
|
||||
@@ -540,7 +603,8 @@ pub(super) fn material_program_for_corners(
|
||||
UniformName::new("shadow_values", UniformType::_2f),
|
||||
];
|
||||
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) => {
|
||||
let program = MaterialProgram(program);
|
||||
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)
|
||||
}
|
||||
|
||||
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![
|
||||
Uniform::new("paint_mode", paint_mode),
|
||||
Uniform::new("shadow_rect", parameters.shadow_rect),
|
||||
@@ -587,9 +654,15 @@ fn panel_uniforms(parameters: &MaterialParameters) -> Vec<Uniform<'static>> {
|
||||
let mut uniforms = vec![
|
||||
Uniform::new(
|
||||
"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) = ¶meters.panel
|
||||
&& !panel.shapes.is_empty()
|
||||
@@ -643,7 +716,11 @@ fn panel_matrices(
|
||||
4 => 1,
|
||||
_ => 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,
|
||||
1 => 4,
|
||||
2 => 3,
|
||||
@@ -707,7 +784,10 @@ mod region_geometry_tests {
|
||||
runtime.panel_style,
|
||||
);
|
||||
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!(
|
||||
material.style,
|
||||
if reduce {
|
||||
@@ -738,9 +818,14 @@ mod region_geometry_tests {
|
||||
let source = format!(
|
||||
"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 material =
|
||||
resolve_surface_material(SemanticRole::Panel, runtime.theme_settings, None, runtime.panel_style);
|
||||
let (_, runtime, candidate) =
|
||||
crate::config::prepare(&source, std::path::Path::new("/tmp")).unwrap();
|
||||
let material = resolve_surface_material(
|
||||
SemanticRole::Panel,
|
||||
runtime.theme_settings,
|
||||
None,
|
||||
runtime.panel_style,
|
||||
);
|
||||
assert_eq!(material.shadow, [0.; 3]);
|
||||
assert_eq!(material.opacity, candidate.theme.panel_opacity(None));
|
||||
assert_eq!(
|
||||
@@ -751,7 +836,11 @@ mod region_geometry_tests {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -760,7 +849,10 @@ mod region_geometry_tests {
|
||||
fn framebuffer_transforms_preserve_each_logical_corner() {
|
||||
let radii = [1., 2., 3., 4.];
|
||||
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 [
|
||||
(Transform::Normal, [1., 2., 3., 4.]),
|
||||
(Transform::_90, [4., 1., 2., 3.]),
|
||||
@@ -781,7 +873,9 @@ mod region_geometry_tests {
|
||||
use ferese_panel::shape::PixelGrid;
|
||||
for scale in [1., 1.25, 1.5, 2.] {
|
||||
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(
|
||||
Parameters {
|
||||
style: ferese_panel::PanelStyle::Morph,
|
||||
@@ -814,7 +908,8 @@ mod region_geometry_tests {
|
||||
let [body, left, right] = shapes.as_slice() else {
|
||||
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!(
|
||||
body[0], client_left as f32,
|
||||
"native fill must align with the shell outline"
|
||||
@@ -864,7 +959,9 @@ mod region_geometry_tests {
|
||||
attached: true,
|
||||
edge,
|
||||
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()
|
||||
},
|
||||
bounds,
|
||||
@@ -916,14 +1013,23 @@ mod region_geometry_tests {
|
||||
#[test]
|
||||
fn fractional_regions_snap_once_in_output_pixels() {
|
||||
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();
|
||||
assert_eq!(corners.rect, Rectangle::new((1, 1).into(), (150, 60).into()));
|
||||
let (_, corners) =
|
||||
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);
|
||||
// The old logical rounding would put this at physical (0, 0).
|
||||
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.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()
|
||||
);
|
||||
}
|
||||
}
|
||||
+154
-54
@@ -19,14 +19,17 @@ use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
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 materials::*;
|
||||
use overview::*;
|
||||
pub(crate) use resources::RenderResources;
|
||||
use scene::cursor_elements;
|
||||
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;
|
||||
use smithay::backend::allocator::Fourcc;
|
||||
@@ -36,40 +39,50 @@ use smithay::backend::renderer::element::surface::{
|
||||
WaylandSurfaceRenderElement, WaylandSurfaceTexture, render_elements_from_surface_tree,
|
||||
};
|
||||
use smithay::backend::renderer::element::utils::{
|
||||
ConstrainAlign, ConstrainScaleBehavior, CropRenderElement, Relocate, RelocateRenderElement, RescaleRenderElement,
|
||||
constrain_render_elements,
|
||||
ConstrainAlign, ConstrainScaleBehavior, CropRenderElement, Relocate, RelocateRenderElement,
|
||||
RescaleRenderElement, constrain_render_elements,
|
||||
};
|
||||
use smithay::backend::renderer::element::{
|
||||
Element, Id, Kind as RenderElementKind, RenderElement, UnderlyingStorage, render_elements,
|
||||
};
|
||||
use smithay::backend::renderer::gles::{
|
||||
GlesError, GlesFrame, GlesPixelProgram, GlesRenderer, GlesTexProgram, GlesTexture, Uniform, UniformName,
|
||||
UniformType,
|
||||
GlesError, GlesFrame, GlesPixelProgram, GlesRenderer, GlesTexProgram, GlesTexture, Uniform,
|
||||
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::{LayerSurface, PopupManager, layer_map_for_output};
|
||||
use smithay::input::pointer::{CursorImageStatus, CursorImageSurfaceData};
|
||||
use smithay::output::Output;
|
||||
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::shell::wlr_layer::Layer;
|
||||
|
||||
use crate::Ferese;
|
||||
use crate::metrics::FrameEffectMetrics;
|
||||
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 =
|
||||
CropRenderElement<RelocateRenderElement<RescaleRenderElement<WaylandSurfaceRenderElement<GlesRenderer>>>>;
|
||||
type SurfaceRenderElement = CropRenderElement<
|
||||
RelocateRenderElement<RescaleRenderElement<WaylandSurfaceRenderElement<GlesRenderer>>>,
|
||||
>;
|
||||
|
||||
type WindowRenderElement = CropRenderElement<RelocateRenderElement<RescaleRenderElement<WindowContentRenderElement>>>;
|
||||
type WindowRenderElement =
|
||||
CropRenderElement<RelocateRenderElement<RescaleRenderElement<WindowContentRenderElement>>>;
|
||||
|
||||
type MemoryRenderElement =
|
||||
CropRenderElement<RelocateRenderElement<RescaleRenderElement<MemoryRenderBufferRenderElement<GlesRenderer>>>>;
|
||||
type MemoryRenderElement = CropRenderElement<
|
||||
RelocateRenderElement<RescaleRenderElement<MemoryRenderBufferRenderElement<GlesRenderer>>>,
|
||||
>;
|
||||
|
||||
render_elements! {
|
||||
pub(crate) AnimatedWindowRenderElement<=GlesRenderer>;
|
||||
@@ -127,7 +140,9 @@ fn corner_shader(source: &str) -> String {
|
||||
|
||||
fn corner_shader_for(source: &str, shape: CornerShape) -> String {
|
||||
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
|
||||
.replace(
|
||||
"//_CORNERS_",
|
||||
@@ -138,7 +153,10 @@ fn corner_shader_for(source: &str, shape: CornerShape) -> String {
|
||||
)
|
||||
.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"))
|
||||
}
|
||||
|
||||
@@ -216,7 +234,11 @@ impl Element for BlurRenderElement {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -517,7 +539,11 @@ impl Element for RoundedSurfaceRenderElement {
|
||||
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, || {
|
||||
self.inner.damage_since(scale, commit)
|
||||
})
|
||||
@@ -634,8 +660,8 @@ mod tests {
|
||||
use smithay::utils::{Buffer, Logical, Physical, Rectangle, Scale, Transform};
|
||||
|
||||
use super::{
|
||||
border_gradient_line, color_with_alpha, framebuffer_clip_rect, resize_content_behavior, rounded_visual_rect,
|
||||
scaled_visual_rect, shadow_bounds,
|
||||
border_gradient_line, color_with_alpha, framebuffer_clip_rect, resize_content_behavior,
|
||||
rounded_visual_rect, scaled_visual_rect, shadow_bounds,
|
||||
};
|
||||
|
||||
#[test]
|
||||
@@ -643,9 +669,14 @@ mod tests {
|
||||
fn solid_corner_mask_preserves_premultiplied_color() {
|
||||
use smithay::backend::allocator::Fourcc;
|
||||
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};
|
||||
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 context = EGLContext::new(&display).unwrap();
|
||||
let mut renderer = unsafe { GlesRenderer::new(context).unwrap() };
|
||||
@@ -662,7 +693,9 @@ mod tests {
|
||||
let size = (32, 32).into();
|
||||
let mut texture: GlesTexture = renderer.create_buffer(Fourcc::Abgr8888, size).unwrap();
|
||||
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 frame = renderer
|
||||
@@ -678,7 +711,10 @@ mod tests {
|
||||
Some(&[rect]),
|
||||
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("color", [0.5f32, 0.0, 0.0, 0.5]),
|
||||
],
|
||||
@@ -702,7 +738,10 @@ mod tests {
|
||||
fn shared_corner_shaders_compile() {
|
||||
use smithay::backend::egl::{EGLContext, EGLDevice, EGLDisplay};
|
||||
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 context = EGLContext::new(&display).unwrap();
|
||||
let mut renderer = unsafe { GlesRenderer::new(context).unwrap() };
|
||||
@@ -729,22 +768,33 @@ mod tests {
|
||||
use smithay::backend::allocator::Fourcc;
|
||||
use smithay::backend::egl::{EGLContext, EGLDevice, EGLDisplay};
|
||||
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 context = EGLContext::new(&display).unwrap();
|
||||
let mut renderer = unsafe { GlesRenderer::new(context).unwrap() };
|
||||
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();
|
||||
let size = (32, 32).into();
|
||||
let mut pixels = vec![128u8; 32 * 32 * 4];
|
||||
for pixel in pixels.as_chunks_mut::<4>().0 {
|
||||
pixel[3] = 255;
|
||||
}
|
||||
let blurred = renderer.import_memory(&pixels, Fourcc::Abgr8888, size, false).unwrap();
|
||||
let mut target_texture: GlesTexture = renderer.create_buffer(Fourcc::Abgr8888, size).unwrap();
|
||||
let blurred = renderer
|
||||
.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());
|
||||
|
||||
for (opacity, strength, tint) in [
|
||||
@@ -768,7 +818,9 @@ mod tests {
|
||||
let mut frame = renderer
|
||||
.render(&mut target, (32, 32).into(), Transform::Normal)
|
||||
.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
|
||||
.draw_solid(
|
||||
Rectangle::new((0, 0).into(), (16, 32).into()),
|
||||
@@ -801,7 +853,8 @@ mod tests {
|
||||
left.abs_diff(right) <= 2,
|
||||
"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!(
|
||||
left.abs_diff(expected) <= 2,
|
||||
"opacity {opacity} failed to control tint: {left} versus {expected}"
|
||||
@@ -823,7 +876,8 @@ mod tests {
|
||||
Transform::Flipped270,
|
||||
] {
|
||||
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 [
|
||||
(0.0, (110.0, 360.0), (910.0, 360.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.y - f64::from(line[1])) * f64::from(line[3])
|
||||
};
|
||||
assert!(progress(first.into()).abs() < 0.00001, "{transform:?}, {angle}");
|
||||
assert!((progress(last.into()) - 1.0).abs() < 0.00001, "{transform:?}, {angle}");
|
||||
assert!(
|
||||
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> {
|
||||
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> {
|
||||
@@ -904,9 +969,9 @@ mod tests {
|
||||
if commit == Some(self.current_commit()) {
|
||||
smithay::backend::renderer::utils::DamageSet::default()
|
||||
} else {
|
||||
smithay::backend::renderer::utils::DamageSet::from_slice(&[Rectangle::from_size(
|
||||
self.geometry(scale).size,
|
||||
)])
|
||||
smithay::backend::renderer::utils::DamageSet::from_slice(&[
|
||||
Rectangle::from_size(self.geometry(scale).size),
|
||||
])
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -933,7 +998,12 @@ mod tests {
|
||||
element.clip_changed = resources.thumbnail_corner_changed(window, output, shape);
|
||||
assert_eq!(element.commit, client_commit);
|
||||
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((299, 259))));
|
||||
element.clip_changed = resources.thumbnail_corner_changed(window, output, shape);
|
||||
@@ -949,7 +1019,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
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 width in [300, 600, 1200] {
|
||||
let source = Rectangle::<i32, Logical>::from_size((600, 800).into());
|
||||
@@ -1020,13 +1092,17 @@ mod tests {
|
||||
Transform::Flipped180,
|
||||
Transform::Flipped270,
|
||||
] {
|
||||
let output_size =
|
||||
smithay::utils::Size::<i32, Physical>::from(((1280.0 * scale) as i32, (800.0 * scale) as i32));
|
||||
let output_size = smithay::utils::Size::<i32, Physical>::from((
|
||||
(1280.0 * scale) as i32,
|
||||
(800.0 * scale) as i32,
|
||||
));
|
||||
let element_size = transform.transform_size(output_size);
|
||||
let logical_height = f64::from(element_size.h) / scale;
|
||||
// Bar and exclusion end at 40; the 10px outer gap ends at 50.
|
||||
let geometry =
|
||||
Rectangle::<f64, Logical>::new((10.0, 50.0).into(), (400.0, logical_height - 60.0).into())
|
||||
let geometry = Rectangle::<f64, Logical>::new(
|
||||
(10.0, 50.0).into(),
|
||||
(400.0, logical_height - 60.0).into(),
|
||||
)
|
||||
.to_physical_precise_round(scale);
|
||||
let clip = framebuffer_clip_rect(geometry, output_size, transform);
|
||||
// Independently reproduce GlesFrame's orthographic projection,
|
||||
@@ -1048,7 +1124,10 @@ mod tests {
|
||||
let max_x = xs.iter().copied().fold(f32::NEG_INFINITY, f32::max);
|
||||
let min_y = ys.iter().copied().fold(f32::INFINITY, f32::min);
|
||||
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!(
|
||||
(actual - expected).abs() < 0.001,
|
||||
"{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 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 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();
|
||||
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((735, 519))));
|
||||
@@ -1098,13 +1183,22 @@ mod tests {
|
||||
#[test]
|
||||
fn translucent_materials_blur_at_full_opacity() {
|
||||
use crate::config::MaterialStyle;
|
||||
assert_eq!(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!(
|
||||
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!(
|
||||
super::material_blur_radius(MaterialStyle::Translucent, 0.79, 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]
|
||||
@@ -1120,7 +1214,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
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]
|
||||
@@ -1134,7 +1231,10 @@ mod tests {
|
||||
|
||||
for tenth in 0..=10 {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -19,7 +19,11 @@ pub(super) fn overview_chrome_element(
|
||||
let mode = output.current_mode()?;
|
||||
let parameters = BorderParameters {
|
||||
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,
|
||||
width: (width * scale) as f32,
|
||||
color,
|
||||
@@ -30,13 +34,19 @@ pub(super) fn overview_chrome_element(
|
||||
focus_gradient_line: [0.0; 4],
|
||||
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
|
||||
} else {
|
||||
material_program(&mut state.render, renderer)?.0
|
||||
};
|
||||
let uniforms = |p: &BorderParameters| {
|
||||
if matches!(key.1, OverviewChromePart::Outline | OverviewChromePart::TabListOutline) {
|
||||
if matches!(
|
||||
key.1,
|
||||
OverviewChromePart::Outline | OverviewChromePart::TabListOutline
|
||||
) {
|
||||
border_uniforms(p)
|
||||
} else {
|
||||
tint_uniforms(p)
|
||||
@@ -47,7 +57,9 @@ pub(super) fn overview_chrome_element(
|
||||
.render
|
||||
.outputs
|
||||
.entry(output_id)
|
||||
.or_insert_with(|| OverviewScrim { chrome: HashMap::new() })
|
||||
.or_insert_with(|| OverviewScrim {
|
||||
chrome: HashMap::new(),
|
||||
})
|
||||
.chrome;
|
||||
let cached = chrome
|
||||
.entry(key)
|
||||
@@ -129,7 +141,9 @@ pub(super) fn overview_strip_elements(
|
||||
| OverviewChromePart::TabListOutline
|
||||
| OverviewChromePart::TabList
|
||||
| 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::Strip => true,
|
||||
});
|
||||
@@ -149,7 +163,11 @@ pub(super) fn overview_strip_elements(
|
||||
{
|
||||
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;
|
||||
};
|
||||
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,
|
||||
);
|
||||
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,
|
||||
));
|
||||
if let Ok(element) = MemoryRenderBufferRenderElement::from_buffer(
|
||||
@@ -171,7 +191,9 @@ pub(super) fn overview_strip_elements(
|
||||
location.to_f64(),
|
||||
&buffer,
|
||||
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(
|
||||
(
|
||||
(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 = RelocateRenderElement::from_element(element, (0, 0), Relocate::Relative);
|
||||
if let Some(element) =
|
||||
CropRenderElement::from_element(element, scale, physical_rect(caption, output_geometry.loc, scale))
|
||||
{
|
||||
if let Some(element) = CropRenderElement::from_element(
|
||||
element,
|
||||
scale,
|
||||
physical_rect(caption, output_geometry.loc, scale),
|
||||
) {
|
||||
elements.push(element.into());
|
||||
}
|
||||
}
|
||||
@@ -235,12 +259,19 @@ pub(super) fn overview_strip_elements(
|
||||
let shape = window_corner_shape(window);
|
||||
|
||||
if let Some(programs) = corner_program(&mut state.render, renderer, shape) {
|
||||
let clip_changed = state
|
||||
.output_id(output)
|
||||
.is_some_and(|output| state.render.thumbnail_corner_changed(*id, output, shape));
|
||||
let Some(sample) = frame.windows.get(id) else { continue };
|
||||
let clip_changed = state.output_id(output).is_some_and(|output| {
|
||||
state.render.thumbnail_corner_changed(*id, output, shape)
|
||||
});
|
||||
let Some(sample) = frame.windows.get(id) else {
|
||||
continue;
|
||||
};
|
||||
let presentation = sample.presentation.thumbnail(*rect);
|
||||
let corners = RoundedRect::new(presentation.bounds.current, output_geometry.loc, scale, 4.0)
|
||||
let corners = RoundedRect::new(
|
||||
presentation.bounds.current,
|
||||
output_geometry.loc,
|
||||
scale,
|
||||
4.0,
|
||||
)
|
||||
.with_shape(shape);
|
||||
if let Some(snapshot) = state.render.snapshot(id)
|
||||
&& snapshot.context == renderer.context_id().erased()
|
||||
@@ -277,16 +308,20 @@ pub(super) fn overview_strip_elements(
|
||||
}
|
||||
if let Some((buffer, size)) = state.overview_workspace_label(card.index, scale) {
|
||||
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,
|
||||
((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(
|
||||
renderer,
|
||||
location.to_f64(),
|
||||
&buffer,
|
||||
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(
|
||||
(
|
||||
(f64::from(size.w) / scale).round() as i32,
|
||||
@@ -297,7 +332,8 @@ pub(super) fn overview_strip_elements(
|
||||
RenderElementKind::Unspecified,
|
||||
) {
|
||||
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(
|
||||
element,
|
||||
scale,
|
||||
|
||||
@@ -60,7 +60,12 @@ impl RenderResources {
|
||||
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
|
||||
.entry(id)
|
||||
.or_default()
|
||||
@@ -117,7 +122,9 @@ impl RenderResources {
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -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 {
|
||||
if let Some(snapshot) = &mut window.snapshot
|
||||
&& !keep(id, snapshot)
|
||||
@@ -159,7 +169,8 @@ impl RenderResources {
|
||||
.filter(|window| &window.snapshot.context == context)
|
||||
.map(|window| window.presentation.id)
|
||||
.collect();
|
||||
self.closing.retain(|window| &window.snapshot.context != context);
|
||||
self.closing
|
||||
.retain(|window| &window.snapshot.context != context);
|
||||
for id in removed {
|
||||
self.remove_window(id);
|
||||
}
|
||||
@@ -201,8 +212,12 @@ mod tests {
|
||||
#[test]
|
||||
fn window_removal_releases_all_its_caches_without_removing_other_windows() {
|
||||
let mut resources = RenderResources::default();
|
||||
resources.windows.insert(WindowId(1), WindowBuffers::default());
|
||||
resources.windows.insert(WindowId(2), WindowBuffers::default());
|
||||
resources
|
||||
.windows
|
||||
.insert(WindowId(1), WindowBuffers::default());
|
||||
resources
|
||||
.windows
|
||||
.insert(WindowId(2), WindowBuffers::default());
|
||||
|
||||
resources.remove_window(WindowId(1));
|
||||
resources.clear_snapshot(&WindowId(1));
|
||||
@@ -217,7 +232,10 @@ mod tests {
|
||||
fn context_removal_evicts_programs_and_snapshots_without_touching_another_gpu() {
|
||||
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 mut first = 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();
|
||||
material_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(
|
||||
id,
|
||||
ResizeSnapshot {
|
||||
|
||||
@@ -10,7 +10,10 @@ pub(crate) fn animated_window_elements(
|
||||
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
|
||||
}
|
||||
|
||||
@@ -69,8 +72,15 @@ fn scene_elements(
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let background = state.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])
|
||||
let background = state
|
||||
.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);
|
||||
let opacity = state.session_lock.idle_opacity;
|
||||
@@ -87,7 +97,13 @@ fn scene_elements(
|
||||
});
|
||||
overlay.resize(geometry.size);
|
||||
elements.push(
|
||||
SolidColorRenderElement::from_buffer(overlay, (0, 0), scale, opacity, RenderElementKind::Unspecified)
|
||||
SolidColorRenderElement::from_buffer(
|
||||
overlay,
|
||||
(0, 0),
|
||||
scale,
|
||||
opacity,
|
||||
RenderElementKind::Unspecified,
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
@@ -98,7 +114,10 @@ fn scene_elements(
|
||||
.filter(|surface| surface.alive())
|
||||
{
|
||||
elements.extend(
|
||||
render_elements_from_surface_tree::<GlesRenderer, WaylandSurfaceRenderElement<GlesRenderer>>(
|
||||
render_elements_from_surface_tree::<
|
||||
GlesRenderer,
|
||||
WaylandSurfaceRenderElement<GlesRenderer>,
|
||||
>(
|
||||
renderer,
|
||||
surface.wl_surface(),
|
||||
(0, 0),
|
||||
@@ -111,7 +130,14 @@ fn scene_elements(
|
||||
);
|
||||
}
|
||||
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;
|
||||
}
|
||||
@@ -126,7 +152,8 @@ fn scene_elements(
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let upper_layers: &[Layer] = if !state.layer_visible_for_frame(output, Layer::Top, frame.overview.is_presenting()) {
|
||||
let upper_layers: &[Layer] =
|
||||
if !state.layer_visible_for_frame(output, Layer::Top, frame.overview.is_presenting()) {
|
||||
&[Layer::Overlay]
|
||||
} else {
|
||||
&[Layer::Overlay, Layer::Top]
|
||||
@@ -196,7 +223,9 @@ fn scene_elements(
|
||||
if let Some(group) = closing.remove(&Some(id)) {
|
||||
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 material_surface = window
|
||||
.toplevel()
|
||||
@@ -211,7 +240,8 @@ fn scene_elements(
|
||||
state.theme_settings.window_radius
|
||||
} * decoration_progress;
|
||||
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 rounded_clip_program = corner_program(&mut state.render, renderer, shape);
|
||||
// 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 focus = sample.presentation.focus();
|
||||
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 gradient = state.theme_settings.border_gradient;
|
||||
|
||||
@@ -273,7 +304,10 @@ fn scene_elements(
|
||||
scale,
|
||||
shadow_offset_y * offset_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,
|
||||
output,
|
||||
&programs,
|
||||
@@ -313,9 +347,16 @@ fn scene_elements(
|
||||
{
|
||||
let mut color = state.theme_settings.surface_base_color.0;
|
||||
color[3] = presentation_alpha;
|
||||
if let Some(fill) =
|
||||
window_tint_element(&mut state.render, renderer, id, constrain, corners, color, true, output)
|
||||
{
|
||||
if let Some(fill) = window_tint_element(
|
||||
&mut state.render,
|
||||
renderer,
|
||||
id,
|
||||
constrain,
|
||||
corners,
|
||||
color,
|
||||
true,
|
||||
output,
|
||||
) {
|
||||
// Front-to-back: fill uncovered strips behind the native
|
||||
// content instead of stretching it or exposing wallpaper.
|
||||
elements.push(fill.into());
|
||||
@@ -342,8 +383,13 @@ fn scene_elements(
|
||||
elements.push(background);
|
||||
}
|
||||
if material_surface.is_none()
|
||||
&& let Some(blur) =
|
||||
super::window_blur::window_blur_element(state, renderer, output, sample.presentation, corners)
|
||||
&& let Some(blur) = super::window_blur::window_blur_element(
|
||||
state,
|
||||
renderer,
|
||||
output,
|
||||
sample.presentation,
|
||||
corners,
|
||||
)
|
||||
{
|
||||
elements.push(blur);
|
||||
}
|
||||
@@ -420,20 +466,22 @@ pub(super) fn layer_elements(
|
||||
requested_layers
|
||||
.iter()
|
||||
.flat_map(|requested| {
|
||||
map.layers_on(*requested)
|
||||
.rev()
|
||||
.filter_map(|layer| map.layer_geometry(layer).map(|geometry| (geometry, layer.clone())))
|
||||
map.layers_on(*requested).rev().filter_map(|layer| {
|
||||
map.layer_geometry(layer)
|
||||
.map(|geometry| (geometry, layer.clone()))
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
state.render.surfaces.retain(|surface, _| {
|
||||
surface.is_alive()
|
||||
|| state
|
||||
.render
|
||||
.closing
|
||||
.iter()
|
||||
.any(|window| window.material.as_ref().is_some_and(|(key, ..)| key == surface))
|
||||
|| state.render.closing.iter().any(|window| {
|
||||
window
|
||||
.material
|
||||
.as_ref()
|
||||
.is_some_and(|(key, ..)| key == surface)
|
||||
})
|
||||
});
|
||||
|
||||
let mut elements = Vec::new();
|
||||
@@ -486,7 +534,8 @@ pub(super) fn cursor_elements(
|
||||
let Some(pointer) = state.seat.get_pointer() else {
|
||||
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 pointer_location = pointer.current_location() - output_geometry.loc.to_f64();
|
||||
match &state.cursor_status {
|
||||
@@ -498,9 +547,13 @@ pub(super) fn cursor_elements(
|
||||
.map(|attributes| attributes.lock().unwrap().hotspot)
|
||||
.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,
|
||||
surface,
|
||||
physical_location,
|
||||
@@ -511,7 +564,8 @@ pub(super) fn cursor_elements(
|
||||
.into_iter()
|
||||
.filter_map(|element| {
|
||||
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)
|
||||
})
|
||||
@@ -573,7 +627,8 @@ mod layer_crop_tests {
|
||||
[1.0; 4],
|
||||
RenderElementKind::Unspecified,
|
||||
);
|
||||
let crop = CropRenderElement::from_element(panel, 1.0, layer_output_crop(mode, transform))
|
||||
let crop =
|
||||
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:?}");
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
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::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();
|
||||
|
||||
if self.state.area != area || self.state.opaque != opaque {
|
||||
@@ -75,7 +81,13 @@ impl Element for SharedPixelShaderElement {
|
||||
}
|
||||
|
||||
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> {
|
||||
@@ -159,9 +171,13 @@ mod tests {
|
||||
#[test]
|
||||
#[ignore = "requires an EGL rendering device"]
|
||||
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 mut renderer = unsafe { GlesRenderer::new(EGLContext::new(&display).unwrap()).unwrap() };
|
||||
let mut renderer =
|
||||
unsafe { GlesRenderer::new(EGLContext::new(&display).unwrap()).unwrap() };
|
||||
let program = renderer
|
||||
.compile_custom_pixel_shader(
|
||||
super::super::corner_shader(super::super::ROUNDED_SOLID_SHADER),
|
||||
@@ -191,7 +207,14 @@ mod tests {
|
||||
uniforms(red),
|
||||
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_commit = old.current_commit();
|
||||
|
||||
@@ -205,14 +228,20 @@ mod tests {
|
||||
);
|
||||
|
||||
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!(
|
||||
&*shared.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())]));
|
||||
assert!(Arc::ptr_eq(&shared.state, &old.state));
|
||||
shared.update_uniforms(uniforms(green));
|
||||
@@ -220,12 +249,21 @@ mod tests {
|
||||
assert_eq!(shared.id(), old.id());
|
||||
assert_eq!(old.current_commit(), old_commit);
|
||||
assert_ne!(shared.current_commit(), old_commit);
|
||||
assert_eq!(pixels(&mut renderer, &old), pixels(&mut renderer, &baseline));
|
||||
assert_ne!(pixels(&mut renderer, &shared), pixels(&mut renderer, &baseline));
|
||||
assert_eq!(
|
||||
pixels(&mut renderer, &old),
|
||||
pixels(&mut renderer, &baseline)
|
||||
);
|
||||
assert_ne!(
|
||||
pixels(&mut renderer, &shared),
|
||||
pixels(&mut renderer, &baseline)
|
||||
);
|
||||
|
||||
let retained = shared.clone();
|
||||
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!(shared.opaque_regions(1.0.into()).is_empty());
|
||||
assert_eq!(shared.alpha(), baseline.alpha());
|
||||
|
||||
@@ -13,14 +13,23 @@ fn text_element(
|
||||
centered: bool,
|
||||
) -> Option<AnimatedWindowRenderElement> {
|
||||
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 mut title: String = text.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 title: String = text
|
||||
.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();
|
||||
boundaries.push(title.len());
|
||||
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 {
|
||||
title.clear();
|
||||
@@ -37,14 +46,17 @@ fn text_element(
|
||||
};
|
||||
let location = Point::<i32, Physical>::from((
|
||||
((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(
|
||||
renderer,
|
||||
location.to_f64(),
|
||||
&buffer,
|
||||
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(
|
||||
(
|
||||
(f64::from(size.w) / scale).round() as i32,
|
||||
@@ -57,12 +69,23 @@ fn text_element(
|
||||
.ok()?;
|
||||
let element = RescaleRenderElement::from_element(element, location, 1.0);
|
||||
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);
|
||||
CropRenderElement::from_element(element, scale, physical_rect(clip, output_origin, scale)).map(Into::into)
|
||||
let clip = Rect::new(
|
||||
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 {
|
||||
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();
|
||||
}
|
||||
state
|
||||
@@ -99,8 +122,12 @@ pub(super) fn elements(
|
||||
let scale = output.current_scale().fractional_scale();
|
||||
let settings = state.tab_indicator_settings;
|
||||
let anchor = bar.group.windows[0];
|
||||
let accent = settings.active_color.unwrap_or(state.theme_settings.accent_color.0);
|
||||
let inactive = settings.inactive_color.unwrap_or(state.theme_settings.border_color.0);
|
||||
let accent = settings
|
||||
.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
|
||||
.background_color
|
||||
.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.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
|
||||
} else {
|
||||
@@ -135,7 +163,15 @@ pub(super) fn elements(
|
||||
.tab_indicator
|
||||
.first_row
|
||||
.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 label = title(state, id, capture);
|
||||
if let Some(element) = text_element(
|
||||
@@ -153,7 +189,9 @@ pub(super) fn elements(
|
||||
if id == bar.group.active {
|
||||
// Match the control center's selected shell buttons: a quiet
|
||||
// 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;
|
||||
if let Some(element) = overview_chrome_element(
|
||||
state,
|
||||
@@ -172,7 +210,9 @@ pub(super) fn elements(
|
||||
}
|
||||
}
|
||||
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(
|
||||
state,
|
||||
renderer,
|
||||
@@ -224,7 +264,8 @@ pub(super) fn elements(
|
||||
output_geometry.size.h as f64,
|
||||
);
|
||||
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),
|
||||
width,
|
||||
g.row_height,
|
||||
@@ -263,7 +304,11 @@ pub(super) fn elements(
|
||||
g.rail.width,
|
||||
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(
|
||||
state,
|
||||
renderer,
|
||||
|
||||
Loaded 100 of 336 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user