feat: gate semantic effects by client capability
This commit is contained in:
1 parent
6d19932a64
commit
a6ec721437
9 files changed
+387
-5
No files matched your search
@@ -11,6 +11,7 @@ ferese-animation = { path = "../crates/ferese-animation" }
|
||||
ferese-core = { path = "../crates/ferese-core" }
|
||||
ferese-ipc = { path = "../crates/ferese-ipc" }
|
||||
ferese-layout = { path = "../crates/ferese-layout" }
|
||||
ferese-protocols = { path = "../crates/ferese-protocols", features = ["server"] }
|
||||
libc.workspace = true
|
||||
smithay.workspace = true
|
||||
serde.workspace = true
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::ffi::OsString;
|
||||
|
||||
use smithay::reexports::calloop::EventLoop;
|
||||
|
||||
use crate::Ferese;
|
||||
use crate::{Ferese, private_client::ClientCapabilities};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum BackendKind {
|
||||
@@ -16,6 +16,7 @@ pub enum BackendKind {
|
||||
pub struct LaunchConfig {
|
||||
pub backend: BackendKind,
|
||||
pub client: Vec<OsString>,
|
||||
pub client_capabilities: ClientCapabilities,
|
||||
}
|
||||
|
||||
impl LaunchConfig {
|
||||
@@ -33,6 +34,7 @@ impl LaunchConfig {
|
||||
let mut args = args.into_iter().peekable();
|
||||
let mut requested_backend = None;
|
||||
let mut client = Vec::new();
|
||||
let mut client_capabilities = ClientCapabilities::default();
|
||||
|
||||
while let Some(argument) = args.next() {
|
||||
if argument == "--" {
|
||||
@@ -46,6 +48,10 @@ impl LaunchConfig {
|
||||
requested_backend = Some(parse_backend(&value)?);
|
||||
continue;
|
||||
}
|
||||
if argument == "--grant-effects" {
|
||||
client_capabilities.insert(ClientCapabilities::EFFECTS);
|
||||
continue;
|
||||
}
|
||||
if let Some(value) = argument
|
||||
.to_str()
|
||||
.and_then(|argument| argument.strip_prefix("--backend="))
|
||||
@@ -65,7 +71,11 @@ impl LaunchConfig {
|
||||
BackendKind::Drm
|
||||
});
|
||||
|
||||
Ok(Self { backend, client })
|
||||
Ok(Self {
|
||||
backend,
|
||||
client,
|
||||
client_capabilities,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,6 +112,7 @@ mod tests {
|
||||
|
||||
assert_eq!(config.backend, BackendKind::Nested);
|
||||
assert_eq!(config.client, [OsString::from("foot")]);
|
||||
assert!(config.client_capabilities.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -110,6 +121,7 @@ mod tests {
|
||||
|
||||
assert_eq!(config.backend, BackendKind::Drm);
|
||||
assert!(config.client.is_empty());
|
||||
assert!(config.client_capabilities.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -130,6 +142,26 @@ mod tests {
|
||||
config.client,
|
||||
[OsString::from("foot"), OsString::from("--title=Ferese")]
|
||||
);
|
||||
assert!(config.client_capabilities.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicitly_grants_effects_to_the_launched_private_client() {
|
||||
let config = LaunchConfig::parse(
|
||||
[
|
||||
OsString::from("--grant-effects"),
|
||||
OsString::from("ferese-effects-probe"),
|
||||
],
|
||||
true,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(config.client, [OsString::from("ferese-effects-probe")]);
|
||||
assert!(
|
||||
config
|
||||
.client_capabilities
|
||||
.contains(ClientCapabilities::EFFECTS)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
use std::sync::{
|
||||
Mutex,
|
||||
atomic::{AtomicBool, AtomicU64, Ordering},
|
||||
};
|
||||
|
||||
use ferese_protocols::effects::v1::server::{
|
||||
ferese_effects_manager_v1::{self, FereseEffectsManagerV1},
|
||||
ferese_surface_effects_v1::{self, FereseSurfaceEffectsV1},
|
||||
};
|
||||
use smithay::{
|
||||
reexports::wayland_server::{
|
||||
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, WEnum, Weak,
|
||||
backend::ClientId, protocol::wl_surface::WlSurface,
|
||||
},
|
||||
wayland::compositor::with_states,
|
||||
};
|
||||
|
||||
use crate::{Ferese, private_client::ClientCapabilities, state::ClientState};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(crate) enum SemanticRole {
|
||||
Panel,
|
||||
PanelElevated,
|
||||
Popover,
|
||||
Menu,
|
||||
Notification,
|
||||
Hud,
|
||||
Modal,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct SurfaceEffectsState {
|
||||
attached: AtomicBool,
|
||||
generation: AtomicU64,
|
||||
role: Mutex<Option<SemanticRole>>,
|
||||
}
|
||||
|
||||
impl SurfaceEffectsState {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
attached: AtomicBool::new(false),
|
||||
generation: AtomicU64::new(0),
|
||||
role: Mutex::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_role(&self, role: Option<SemanticRole>) {
|
||||
let mut current = self.role.lock().unwrap();
|
||||
if *current != role {
|
||||
*current = role;
|
||||
self.generation.fetch_add(1, Ordering::Release);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct SurfaceEffectsUserData(Mutex<Weak<WlSurface>>);
|
||||
|
||||
impl SurfaceEffectsUserData {
|
||||
fn new(surface: WlSurface) -> Self {
|
||||
Self(Mutex::new(surface.downgrade()))
|
||||
}
|
||||
|
||||
fn surface(&self) -> Option<WlSurface> {
|
||||
self.0.lock().unwrap().upgrade().ok()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn init_global(display: &DisplayHandle) {
|
||||
display.create_global::<Ferese, FereseEffectsManagerV1, _>(1, ());
|
||||
}
|
||||
|
||||
// Consumed by the material renderer in the next M7 slice.
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn surface_role(surface: &WlSurface) -> Option<(SemanticRole, u64)> {
|
||||
with_states(surface, |states| {
|
||||
let effects = states.data_map.get::<SurfaceEffectsState>()?;
|
||||
let role = *effects.role.lock().unwrap();
|
||||
role.map(|role| (role, effects.generation.load(Ordering::Acquire)))
|
||||
})
|
||||
}
|
||||
|
||||
impl GlobalDispatch<FereseEffectsManagerV1, ()> for Ferese {
|
||||
fn bind(
|
||||
_state: &mut Self,
|
||||
_display: &DisplayHandle,
|
||||
_client: &Client,
|
||||
resource: New<FereseEffectsManagerV1>,
|
||||
_global_data: &(),
|
||||
data_init: &mut DataInit<'_, Self>,
|
||||
) {
|
||||
data_init.init(resource, ());
|
||||
}
|
||||
|
||||
fn can_view(client: Client, _global_data: &()) -> bool {
|
||||
client
|
||||
.get_data::<ClientState>()
|
||||
.is_some_and(|state| state.capabilities.contains(ClientCapabilities::EFFECTS))
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<FereseEffectsManagerV1, ()> for Ferese {
|
||||
fn request(
|
||||
_state: &mut Self,
|
||||
_client: &Client,
|
||||
manager: &FereseEffectsManagerV1,
|
||||
request: ferese_effects_manager_v1::Request,
|
||||
_data: &(),
|
||||
_display: &DisplayHandle,
|
||||
data_init: &mut DataInit<'_, Self>,
|
||||
) {
|
||||
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
|
||||
.get::<SurfaceEffectsState>()
|
||||
.unwrap()
|
||||
.attached
|
||||
.swap(true, Ordering::AcqRel)
|
||||
});
|
||||
|
||||
if already_attached {
|
||||
manager.post_error(
|
||||
ferese_effects_manager_v1::Error::AlreadyConstructed,
|
||||
"wl_surface already has a Ferese effects object",
|
||||
);
|
||||
} else {
|
||||
data_init.init(id, SurfaceEffectsUserData::new(surface));
|
||||
}
|
||||
}
|
||||
ferese_effects_manager_v1::Request::Destroy => {}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<FereseSurfaceEffectsV1, SurfaceEffectsUserData> for Ferese {
|
||||
fn request(
|
||||
_state: &mut Self,
|
||||
_client: &Client,
|
||||
_resource: &FereseSurfaceEffectsV1,
|
||||
request: ferese_surface_effects_v1::Request,
|
||||
data: &SurfaceEffectsUserData,
|
||||
_display: &DisplayHandle,
|
||||
_data_init: &mut DataInit<'_, Self>,
|
||||
) {
|
||||
let Some(surface) = data.surface() else {
|
||||
return;
|
||||
};
|
||||
|
||||
match request {
|
||||
ferese_surface_effects_v1::Request::SetRole { role } => {
|
||||
let role = match role {
|
||||
WEnum::Value(ferese_surface_effects_v1::Role::Panel) => SemanticRole::Panel,
|
||||
WEnum::Value(ferese_surface_effects_v1::Role::PanelElevated) => {
|
||||
SemanticRole::PanelElevated
|
||||
}
|
||||
WEnum::Value(ferese_surface_effects_v1::Role::Popover) => SemanticRole::Popover,
|
||||
WEnum::Value(ferese_surface_effects_v1::Role::Menu) => SemanticRole::Menu,
|
||||
WEnum::Value(ferese_surface_effects_v1::Role::Notification) => {
|
||||
SemanticRole::Notification
|
||||
}
|
||||
WEnum::Value(ferese_surface_effects_v1::Role::Hud) => SemanticRole::Hud,
|
||||
WEnum::Value(ferese_surface_effects_v1::Role::Modal) => SemanticRole::Modal,
|
||||
WEnum::Unknown(_) | WEnum::Value(_) => return,
|
||||
};
|
||||
set_surface_role(&surface, Some(role));
|
||||
}
|
||||
ferese_surface_effects_v1::Request::ClearRole => set_surface_role(&surface, None),
|
||||
ferese_surface_effects_v1::Request::Destroy => detach(&surface),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn destroyed(
|
||||
_state: &mut Self,
|
||||
_client: ClientId,
|
||||
_resource: &FereseSurfaceEffectsV1,
|
||||
data: &SurfaceEffectsUserData,
|
||||
) {
|
||||
if let Some(surface) = data.surface() {
|
||||
detach(&surface);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_surface_role(surface: &WlSurface, role: Option<SemanticRole>) {
|
||||
with_states(surface, |states| {
|
||||
if let Some(effects) = states.data_map.get::<SurfaceEffectsState>() {
|
||||
effects.set_role(role);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn detach(surface: &WlSurface) {
|
||||
with_states(surface, |states| {
|
||||
if let Some(effects) = states.data_map.get::<SurfaceEffectsState>() {
|
||||
effects.set_role(None);
|
||||
effects.attached.store(false, Ordering::Release);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn role_changes_advance_generation_only_when_material_changes() {
|
||||
let effects = SurfaceEffectsState::new();
|
||||
effects.set_role(Some(SemanticRole::Panel));
|
||||
assert_eq!(effects.generation.load(Ordering::Acquire), 1);
|
||||
|
||||
effects.set_role(Some(SemanticRole::Panel));
|
||||
assert_eq!(effects.generation.load(Ordering::Acquire), 1);
|
||||
|
||||
effects.set_role(Some(SemanticRole::Popover));
|
||||
assert_eq!(effects.generation.load(Ordering::Acquire), 2);
|
||||
}
|
||||
}
|
||||
+24
-3
@@ -3,11 +3,13 @@
|
||||
mod backends;
|
||||
mod config;
|
||||
mod cursor;
|
||||
mod effects;
|
||||
mod grabs;
|
||||
mod handlers;
|
||||
mod input;
|
||||
mod ipc;
|
||||
mod metrics;
|
||||
mod private_client;
|
||||
mod stacking;
|
||||
mod state;
|
||||
mod window_rules;
|
||||
@@ -66,7 +68,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
unsafe { std::env::set_var("WAYLAND_DISPLAY", &state.socket_name) };
|
||||
|
||||
info!(socket = ?state.socket_name, backend = ?launch.backend, "Ferese is accepting Wayland clients");
|
||||
spawn_client(launch.client);
|
||||
spawn_client(&mut state, launch.client, launch.client_capabilities);
|
||||
event_loop.run(None, &mut state, |_| {})?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -77,15 +79,34 @@ fn init_logging() {
|
||||
tracing_subscriber::fmt().with_env_filter(filter).init();
|
||||
}
|
||||
|
||||
fn spawn_client(mut args: Vec<std::ffi::OsString>) {
|
||||
fn spawn_client(
|
||||
state: &mut Ferese,
|
||||
mut args: Vec<std::ffi::OsString>,
|
||||
capabilities: private_client::ClientCapabilities,
|
||||
) {
|
||||
if args.is_empty() {
|
||||
info!("no client requested; pass one after `--`, for example `-- foot`");
|
||||
return;
|
||||
}
|
||||
let program = args.remove(0);
|
||||
|
||||
match Command::new(&program).args(args).spawn() {
|
||||
let mut command = Command::new(&program);
|
||||
command.args(args);
|
||||
let private_connection = if capabilities.is_empty() {
|
||||
None
|
||||
} else {
|
||||
match private_client::prepare_command(state, &mut command, capabilities) {
|
||||
Ok(connection) => Some(connection),
|
||||
Err(error) => {
|
||||
warn!(program = ?program, %error, "failed to create private Wayland connection");
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
match command.spawn() {
|
||||
Ok(child) => info!(program = ?program, pid = child.id(), "spawned Wayland client"),
|
||||
Err(error) => warn!(program = ?program, %error, "failed to spawn Wayland client"),
|
||||
}
|
||||
drop(private_connection);
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
use std::{
|
||||
io,
|
||||
os::{
|
||||
fd::AsRawFd,
|
||||
unix::{net::UnixStream, process::CommandExt},
|
||||
},
|
||||
process::Command,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use crate::{Ferese, state::ClientState};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub(crate) struct ClientCapabilities(u8);
|
||||
|
||||
impl ClientCapabilities {
|
||||
pub(crate) const EFFECTS: Self = Self(1 << 0);
|
||||
|
||||
pub(crate) fn contains(self, capability: Self) -> bool {
|
||||
self.0 & capability.0 == capability.0
|
||||
}
|
||||
|
||||
pub(crate) fn insert(&mut self, capability: Self) {
|
||||
self.0 |= capability.0;
|
||||
}
|
||||
|
||||
pub(crate) fn is_empty(self) -> bool {
|
||||
self.0 == 0
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn prepare_command(
|
||||
state: &mut Ferese,
|
||||
command: &mut Command,
|
||||
capabilities: ClientCapabilities,
|
||||
) -> io::Result<UnixStream> {
|
||||
let (server, client) = UnixStream::pair()?;
|
||||
state.display_handle.insert_client(
|
||||
server,
|
||||
Arc::new(ClientState {
|
||||
capabilities,
|
||||
..ClientState::default()
|
||||
}),
|
||||
)?;
|
||||
|
||||
let client_fd = client.as_raw_fd();
|
||||
command.env_remove("WAYLAND_DISPLAY");
|
||||
command.env("WAYLAND_SOCKET", client_fd.to_string());
|
||||
// SAFETY: fcntl(F_SETFD) is async-signal-safe. The closure only clears
|
||||
// CLOEXEC on the socket already owned by this Command's parent process.
|
||||
unsafe {
|
||||
command.pre_exec(move || {
|
||||
if libc::fcntl(client_fd, libc::F_SETFD, 0) == -1 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn capabilities_are_explicit_and_independent() {
|
||||
let mut capabilities = ClientCapabilities::default();
|
||||
assert!(capabilities.is_empty());
|
||||
assert!(!capabilities.contains(ClientCapabilities::EFFECTS));
|
||||
|
||||
capabilities.insert(ClientCapabilities::EFFECTS);
|
||||
assert!(!capabilities.is_empty());
|
||||
assert!(capabilities.contains(ClientCapabilities::EFFECTS));
|
||||
}
|
||||
}
|
||||
@@ -355,6 +355,7 @@ impl Ferese {
|
||||
) -> Result<Self, Box<dyn Error>> {
|
||||
let display_handle = display.handle();
|
||||
crate::handlers::screencopy::init_global(&display_handle);
|
||||
crate::effects::init_global(&display_handle);
|
||||
let alpha_modifier_state = AlphaModifierState::new::<Self>(&display_handle);
|
||||
let compositor_state = CompositorState::new::<Self>(&display_handle);
|
||||
let cursor_shape_state = CursorShapeManagerState::new::<Self>(&display_handle);
|
||||
@@ -1856,6 +1857,7 @@ fn client_size(window: &Window) -> Option<ClientSize> {
|
||||
#[derive(Default)]
|
||||
pub struct ClientState {
|
||||
pub compositor_state: CompositorClientState,
|
||||
pub(crate) capabilities: crate::private_client::ClientCapabilities,
|
||||
}
|
||||
|
||||
impl ClientData for ClientState {
|
||||
|
||||
Reference in new issue
Block a user