feat: gate semantic effects by client capability

This commit is contained in:
Samuel Onoja committed 2026-09-25 05:08:18 +01:00
1 parent 6d19932a64
commit a6ec721437
9 files changed
+387 -5

No files matched your search

+1
View File
@@ -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
+34 -2
View File
@@ -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]
+224
View File
@@ -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
View File
@@ -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);
}
+77
View File
@@ -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));
}
}
+2
View File
@@ -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 {