feat: add private shell control protocol
This commit is contained in:
1 parent
a836b55eb9
commit
75ae44c1d7
12 files changed
+727
No files matched your search
@@ -52,6 +52,10 @@ impl LaunchConfig {
|
||||
client_capabilities.insert(ClientCapabilities::EFFECTS);
|
||||
continue;
|
||||
}
|
||||
if argument == "--grant-shell-control" {
|
||||
client_capabilities.insert(ClientCapabilities::SHELL_CONTROL);
|
||||
continue;
|
||||
}
|
||||
if let Some(value) = argument
|
||||
.to_str()
|
||||
.and_then(|argument| argument.strip_prefix("--backend="))
|
||||
@@ -164,6 +168,30 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicitly_grants_shell_control_to_the_launched_private_client() {
|
||||
let config = LaunchConfig::parse(
|
||||
[
|
||||
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)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_an_unknown_backend() {
|
||||
let error =
|
||||
|
||||
@@ -10,6 +10,7 @@ mod input;
|
||||
mod ipc;
|
||||
mod metrics;
|
||||
mod private_client;
|
||||
mod shell_control;
|
||||
mod stacking;
|
||||
mod state;
|
||||
mod window_rules;
|
||||
|
||||
@@ -15,6 +15,7 @@ pub(crate) struct ClientCapabilities(u8);
|
||||
|
||||
impl ClientCapabilities {
|
||||
pub(crate) const EFFECTS: Self = Self(1 << 0);
|
||||
pub(crate) const SHELL_CONTROL: Self = Self(1 << 1);
|
||||
|
||||
pub(crate) fn contains(self, capability: Self) -> bool {
|
||||
self.0 & capability.0 == capability.0
|
||||
@@ -69,9 +70,15 @@ mod tests {
|
||||
let mut capabilities = ClientCapabilities::default();
|
||||
assert!(capabilities.is_empty());
|
||||
assert!(!capabilities.contains(ClientCapabilities::EFFECTS));
|
||||
assert!(!capabilities.contains(ClientCapabilities::SHELL_CONTROL));
|
||||
|
||||
capabilities.insert(ClientCapabilities::EFFECTS);
|
||||
assert!(!capabilities.is_empty());
|
||||
assert!(capabilities.contains(ClientCapabilities::EFFECTS));
|
||||
assert!(!capabilities.contains(ClientCapabilities::SHELL_CONTROL));
|
||||
|
||||
capabilities.insert(ClientCapabilities::SHELL_CONTROL);
|
||||
assert!(capabilities.contains(ClientCapabilities::EFFECTS));
|
||||
assert!(capabilities.contains(ClientCapabilities::SHELL_CONTROL));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
use ferese_layout::WindowId;
|
||||
use ferese_protocols::shell::v1::server::{
|
||||
ferese_shell_manager_v1::{self, FereseShellManagerV1},
|
||||
ferese_shell_v1::{self, FereseShellV1},
|
||||
};
|
||||
use smithay::{
|
||||
reexports::wayland_server::{
|
||||
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
|
||||
},
|
||||
wayland::{compositor::with_states, shell::xdg::XdgToplevelSurfaceData},
|
||||
};
|
||||
|
||||
use crate::{Ferese, private_client::ClientCapabilities, state::ClientState};
|
||||
|
||||
pub(crate) fn init_global(display: &DisplayHandle) {
|
||||
display.create_global::<Ferese, FereseShellManagerV1, _>(1, ());
|
||||
}
|
||||
|
||||
impl GlobalDispatch<FereseShellManagerV1, ()> for Ferese {
|
||||
fn bind(
|
||||
_state: &mut Self,
|
||||
_display: &DisplayHandle,
|
||||
_client: &Client,
|
||||
resource: New<FereseShellManagerV1>,
|
||||
_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::SHELL_CONTROL)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<FereseShellManagerV1, ()> for Ferese {
|
||||
fn request(
|
||||
state: &mut Self,
|
||||
_client: &Client,
|
||||
_manager: &FereseShellManagerV1,
|
||||
request: ferese_shell_manager_v1::Request,
|
||||
_data: &(),
|
||||
_display: &DisplayHandle,
|
||||
data_init: &mut DataInit<'_, Self>,
|
||||
) {
|
||||
match request {
|
||||
ferese_shell_manager_v1::Request::GetShell { id } => {
|
||||
let shell = data_init.init(id, ());
|
||||
|
||||
shell.capabilities(1);
|
||||
shell.overview_state(u32::from(state.overview_active));
|
||||
state.send_shell_snapshot(&shell);
|
||||
state.shell_resources.push(shell.downgrade());
|
||||
}
|
||||
ferese_shell_manager_v1::Request::Destroy => {}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<FereseShellV1, ()> for Ferese {
|
||||
fn request(
|
||||
state: &mut Self,
|
||||
_client: &Client,
|
||||
shell: &FereseShellV1,
|
||||
request: ferese_shell_v1::Request,
|
||||
_data: &(),
|
||||
_display: &DisplayHandle,
|
||||
_data_init: &mut DataInit<'_, Self>,
|
||||
) {
|
||||
match request {
|
||||
ferese_shell_v1::Request::ActivateWindow {
|
||||
window_hi,
|
||||
window_lo,
|
||||
} => state.handle_shell_window_request(
|
||||
shell,
|
||||
ferese_shell_v1::FailedRequest::ActivateWindow,
|
||||
join_id(window_hi, window_lo),
|
||||
ShellWindowAction::Activate,
|
||||
),
|
||||
ferese_shell_v1::Request::CloseWindow {
|
||||
window_hi,
|
||||
window_lo,
|
||||
} => state.handle_shell_window_request(
|
||||
shell,
|
||||
ferese_shell_v1::FailedRequest::CloseWindow,
|
||||
join_id(window_hi, window_lo),
|
||||
ShellWindowAction::Close,
|
||||
),
|
||||
ferese_shell_v1::Request::EnterOverview => state.set_overview_active(true),
|
||||
ferese_shell_v1::Request::ExitOverview => state.set_overview_active(false),
|
||||
ferese_shell_v1::Request::SelectOverviewWindow {
|
||||
window_hi,
|
||||
window_lo,
|
||||
} => {
|
||||
let id = join_id(window_hi, window_lo);
|
||||
|
||||
if state.activate_managed_window(id) {
|
||||
state.set_overview_active(false);
|
||||
} else {
|
||||
send_request_failed(
|
||||
shell,
|
||||
ferese_shell_v1::FailedRequest::SelectOverviewWindow,
|
||||
id,
|
||||
);
|
||||
}
|
||||
}
|
||||
ferese_shell_v1::Request::Destroy => {}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Ferese {
|
||||
pub(crate) fn send_shell_snapshots(&mut self) {
|
||||
let resources = std::mem::take(&mut self.shell_resources);
|
||||
|
||||
for weak in resources {
|
||||
if let Ok(shell) = weak.upgrade() {
|
||||
self.send_shell_snapshot(&shell);
|
||||
self.shell_resources.push(shell.downgrade());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn send_shell_snapshot(&mut self, shell: &FereseShellV1) {
|
||||
self.shell_snapshot_serial = self.shell_snapshot_serial.wrapping_add(1);
|
||||
let serial = self.shell_snapshot_serial;
|
||||
let snapshots = self.managed_window_snapshots();
|
||||
|
||||
shell.snapshot_begin(serial);
|
||||
for snapshot in snapshots {
|
||||
let (window_hi, window_lo) = split_id(snapshot.id.0);
|
||||
let (workspace_hi, workspace_lo) = split_id(snapshot.workspace);
|
||||
|
||||
shell.window(
|
||||
window_hi,
|
||||
window_lo,
|
||||
workspace_hi,
|
||||
workspace_lo,
|
||||
snapshot.app_id,
|
||||
snapshot.title,
|
||||
snapshot.state,
|
||||
);
|
||||
}
|
||||
shell.snapshot_end(serial);
|
||||
}
|
||||
|
||||
fn managed_window_snapshots(&self) -> Vec<ManagedWindowSnapshot> {
|
||||
let mut snapshots = self
|
||||
.window_ids
|
||||
.iter()
|
||||
.filter_map(|(window, id)| {
|
||||
let workspace = self.workspaces.workspace_for_window(*id)?.0;
|
||||
let toplevel = window.toplevel()?;
|
||||
let (app_id, title) = with_states(toplevel.wl_surface(), |states| {
|
||||
let attributes = states
|
||||
.data_map
|
||||
.get::<XdgToplevelSurfaceData>()
|
||||
.expect("xdg toplevel state exists")
|
||||
.lock()
|
||||
.expect("xdg toplevel state is not poisoned");
|
||||
|
||||
(
|
||||
attributes.app_id.clone().unwrap_or_default(),
|
||||
attributes.title.clone().unwrap_or_default(),
|
||||
)
|
||||
});
|
||||
let mut state = ferese_shell_v1::WindowState::empty();
|
||||
|
||||
if self.focused_window == Some(*id) {
|
||||
state |= ferese_shell_v1::WindowState::Focused;
|
||||
}
|
||||
if self
|
||||
.workspaces
|
||||
.workspace_for_window(*id)
|
||||
.and_then(|workspace| self.workspaces.workspace(workspace))
|
||||
.is_some_and(|workspace| workspace.fullscreen == Some(*id))
|
||||
{
|
||||
state |= ferese_shell_v1::WindowState::Fullscreen;
|
||||
}
|
||||
if self.is_floating_window(window) {
|
||||
state |= ferese_shell_v1::WindowState::Floating;
|
||||
}
|
||||
|
||||
Some(ManagedWindowSnapshot {
|
||||
id: *id,
|
||||
workspace,
|
||||
app_id,
|
||||
title,
|
||||
state,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
snapshots.sort_by_key(|snapshot| snapshot.id.0);
|
||||
snapshots
|
||||
}
|
||||
|
||||
fn handle_shell_window_request(
|
||||
&mut self,
|
||||
shell: &FereseShellV1,
|
||||
request: ferese_shell_v1::FailedRequest,
|
||||
id: WindowId,
|
||||
action: ShellWindowAction,
|
||||
) {
|
||||
let succeeded = match action {
|
||||
ShellWindowAction::Activate => self.activate_managed_window(id),
|
||||
ShellWindowAction::Close => self.close_managed_window(id),
|
||||
};
|
||||
|
||||
if !succeeded {
|
||||
send_request_failed(shell, request, id);
|
||||
}
|
||||
}
|
||||
|
||||
fn set_overview_active(&mut self, active: bool) {
|
||||
if self.overview_active == active {
|
||||
return;
|
||||
}
|
||||
|
||||
self.overview_active = active;
|
||||
self.shell_resources.retain(|resource| {
|
||||
resource.upgrade().is_ok_and(|shell| {
|
||||
shell.overview_state(u32::from(active));
|
||||
true
|
||||
})
|
||||
});
|
||||
crate::backends::direct::render_all(self);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum ShellWindowAction {
|
||||
Activate,
|
||||
Close,
|
||||
}
|
||||
|
||||
struct ManagedWindowSnapshot {
|
||||
id: WindowId,
|
||||
workspace: u64,
|
||||
app_id: String,
|
||||
title: String,
|
||||
state: ferese_shell_v1::WindowState,
|
||||
}
|
||||
|
||||
fn split_id(id: u64) -> (u32, u32) {
|
||||
((id >> 32) as u32, id as u32)
|
||||
}
|
||||
|
||||
fn join_id(hi: u32, lo: u32) -> WindowId {
|
||||
WindowId((u64::from(hi) << 32) | u64::from(lo))
|
||||
}
|
||||
|
||||
fn send_request_failed(
|
||||
shell: &FereseShellV1,
|
||||
request: ferese_shell_v1::FailedRequest,
|
||||
id: WindowId,
|
||||
) {
|
||||
let (window_hi, window_lo) = split_id(id.0);
|
||||
|
||||
shell.request_failed(
|
||||
request,
|
||||
window_hi,
|
||||
window_lo,
|
||||
"unknown or unavailable managed window".to_owned(),
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn window_ids_round_trip_through_protocol_words() {
|
||||
for id in [0, 1, u32::MAX as u64, u64::from(u32::MAX) + 1, u64::MAX] {
|
||||
let (hi, lo) = split_id(id);
|
||||
|
||||
assert_eq!(join_id(hi, lo), WindowId(id));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -154,6 +154,13 @@ pub struct Ferese {
|
||||
_ipc_socket: Option<crate::ipc::IpcSocketGuard>,
|
||||
pending_dmabuf_imports: Vec<(Dmabuf, ImportNotifier)>,
|
||||
pub(crate) pending_screencopies: Vec<crate::handlers::screencopy::PendingScreencopy>,
|
||||
pub(crate) shell_resources: Vec<
|
||||
smithay::reexports::wayland_server::Weak<
|
||||
ferese_protocols::shell::v1::server::ferese_shell_v1::FereseShellV1,
|
||||
>,
|
||||
>,
|
||||
pub(crate) shell_snapshot_serial: u32,
|
||||
pub(crate) overview_active: bool,
|
||||
next_window_id: u64,
|
||||
next_output_id: u64,
|
||||
last_animation_tick: Instant,
|
||||
@@ -360,6 +367,7 @@ impl Ferese {
|
||||
let display_handle = display.handle();
|
||||
crate::handlers::screencopy::init_global(&display_handle);
|
||||
crate::effects::init_global(&display_handle);
|
||||
crate::shell_control::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);
|
||||
@@ -470,6 +478,9 @@ impl Ferese {
|
||||
_ipc_socket: None,
|
||||
pending_dmabuf_imports: Vec::new(),
|
||||
pending_screencopies: Vec::new(),
|
||||
shell_resources: Vec::new(),
|
||||
shell_snapshot_serial: 0,
|
||||
overview_active: false,
|
||||
next_window_id: 1,
|
||||
next_output_id: 1,
|
||||
last_animation_tick: start_time,
|
||||
@@ -1132,6 +1143,7 @@ impl Ferese {
|
||||
}
|
||||
self.scrolling_world_x = scrolling_world_x;
|
||||
self.sync_window_stacking();
|
||||
self.send_shell_snapshots();
|
||||
|
||||
crate::backends::direct::render_all(self);
|
||||
}
|
||||
@@ -1570,6 +1582,59 @@ impl Ferese {
|
||||
crate::backends::direct::render_all(self);
|
||||
}
|
||||
|
||||
pub(crate) fn close_managed_window(&mut self, id: WindowId) -> bool {
|
||||
if !self.window_ids.values().any(|window_id| *window_id == id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if self.animations_enabled {
|
||||
self.closing_windows.entry(id).or_default();
|
||||
crate::backends::direct::render_all(self);
|
||||
} else {
|
||||
self.send_window_close(id);
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
pub(crate) fn activate_managed_window(&mut self, id: WindowId) -> bool {
|
||||
let Some(window) = self
|
||||
.window_ids
|
||||
.iter()
|
||||
.find_map(|(window, window_id)| (*window_id == id).then(|| window.clone()))
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
let Some(workspace) = self.workspaces.workspace_for_window(id) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
if let Some(output) = self.output_workspaces.output_for_workspace(workspace) {
|
||||
self.activate_output_workspace(output, workspace);
|
||||
} else if let Some(output) = self.output_workspaces.focused_output() {
|
||||
if self
|
||||
.output_workspaces
|
||||
.assign_workspace(output, workspace)
|
||||
.is_err()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
self.activate_output_workspace(output, workspace);
|
||||
} else if self.workspaces.activate(workspace).is_err() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if self.workspaces.focus_window(id).is_err() {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.focused_window = Some(id);
|
||||
self.raise_window(&window, true);
|
||||
self.relayout();
|
||||
self.restore_keyboard_focus();
|
||||
true
|
||||
}
|
||||
|
||||
pub(crate) fn closing_visual(&self, id: WindowId) -> (f64, f32) {
|
||||
let Some(animation) = self.closing_windows.get(&id) else {
|
||||
return (1.0, 1.0);
|
||||
|
||||
Reference in new issue
Block a user