Files
feresaria/compositor/src/shell_control.rs
T

501 lines
16 KiB
Rust

use ferese_core::{OutputId, WorkspaceId};
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, _>(2, ());
}
pub(crate) fn config_chunks(source: &str) -> Vec<&str> {
let mut remaining = source;
let mut chunks = Vec::new();
while !remaining.is_empty() {
let mut boundary = remaining.len().min(1024);
while !remaining.is_char_boundary(boundary) {
boundary -= 1;
}
chunks.push(&remaining[..boundary]);
remaining = &remaining[boundary..];
}
chunks
}
pub(crate) fn send_shell_config(shell: &FereseShellV1, source: &str) {
shell.config_begin();
for chunk in config_chunks(source) {
shell.config_chunk(chunk.to_owned());
}
shell.config_end();
}
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);
if shell.version() >= 2
&& let Some(source) = &state.config_source
{
send_shell_config(&shell, source);
}
shell.overview_state(u32::from(state.overview.is_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>,
) {
if state.session_lock.active {
return;
}
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::ActivateWorkspace {
workspace_hi,
workspace_lo,
} => {
let workspace = WorkspaceId(join_u64(workspace_hi, workspace_lo));
if !state.activate_managed_workspace(workspace) {
send_request_failed(
shell,
ferese_shell_v1::FailedRequest::ActivateWorkspace,
workspace.0,
);
}
}
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.select_overview_window(id) {
send_request_failed(
shell,
ferese_shell_v1::FailedRequest::SelectOverviewWindow,
id.0,
);
}
}
ferese_shell_v1::Request::Destroy => {}
_ => unreachable!(),
}
}
}
impl Ferese {
pub(crate) fn send_shell_snapshots(&mut self) {
self.shell_resources
.retain(|resource| resource.upgrade().is_ok());
if self.shell_resources.is_empty() {
self.last_shell_snapshot = None;
return;
}
let snapshot = self.shell_snapshot();
if self.last_shell_snapshot.as_ref() == Some(&snapshot) {
return;
}
for resource in &self.shell_resources {
if let Ok(shell) = resource.upgrade() {
self.shell_snapshot_serial = self.shell_snapshot_serial.wrapping_add(1);
snapshot.send(&shell, self.shell_snapshot_serial);
}
}
self.last_shell_snapshot = Some(snapshot);
}
fn shell_snapshot(&self) -> ShellSnapshot {
ShellSnapshot {
outputs: self.output_snapshots(),
workspaces: self.workspace_snapshots(),
windows: self.managed_window_snapshots(),
}
}
fn send_shell_snapshot(&mut self, shell: &FereseShellV1) {
// New subscribers always receive a complete snapshot. Do not update the
// broadcast cache here: existing subscribers may still need this state.
self.shell_snapshot_serial = self.shell_snapshot_serial.wrapping_add(1);
self.shell_snapshot()
.send(shell, self.shell_snapshot_serial);
}
fn output_snapshots(&self) -> Vec<OutputSnapshot> {
let focused = self.output_workspaces.focused_output();
let mut outputs = self
.output_ids
.iter()
.filter_map(|(output, id)| {
Some(OutputSnapshot {
id: *id,
name: output.name(),
active_workspace: self.output_workspaces.active_workspace(*id)?,
focused: focused == Some(*id),
})
})
.collect::<Vec<_>>();
outputs.sort_by_key(|output| output.id.0);
outputs
}
fn workspace_snapshots(&self) -> Vec<WorkspaceSnapshot> {
let mut workspaces = self
.workspaces
.iter()
.map(|workspace| {
let output = self.output_workspaces.output_for_workspace(workspace.id);
let active = output.is_some_and(|output| {
self.output_workspaces.active_workspace(output) == Some(workspace.id)
});
WorkspaceSnapshot {
id: workspace.id,
output,
name: workspace.name.clone(),
active,
}
})
.collect::<Vec<_>>();
workspaces.sort_by_key(|workspace| workspace.id.0);
workspaces
}
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.0);
}
}
pub(crate) fn notify_overview_state(&mut self) {
let active = self.overview.is_active();
self.shell_resources.retain(|resource| {
resource.upgrade().is_ok_and(|shell| {
shell.overview_state(u32::from(active));
true
})
});
}
}
#[derive(Clone, Copy)]
enum ShellWindowAction {
Activate,
Close,
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct ShellSnapshot {
outputs: Vec<OutputSnapshot>,
workspaces: Vec<WorkspaceSnapshot>,
windows: Vec<ManagedWindowSnapshot>,
}
impl ShellSnapshot {
fn send(&self, shell: &FereseShellV1, serial: u32) {
shell.snapshot_begin(serial);
for output in &self.outputs {
let (output_hi, output_lo) = split_id(output.id.0);
let (workspace_hi, workspace_lo) = split_id(output.active_workspace.0);
shell.output(
output_hi,
output_lo,
output.name.clone(),
workspace_hi,
workspace_lo,
u32::from(output.focused),
);
}
for workspace in &self.workspaces {
let (workspace_hi, workspace_lo) = split_id(workspace.id.0);
let (output_hi, output_lo) = workspace
.output
.map(|output| split_id(output.0))
.unwrap_or_default();
shell.workspace(
workspace_hi,
workspace_lo,
output_hi,
output_lo,
workspace.name.clone(),
u32::from(workspace.active),
);
}
for snapshot in &self.windows {
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.clone(),
snapshot.title.clone(),
snapshot.state,
);
}
shell.snapshot_end(serial);
}
}
#[derive(Clone, Debug, PartialEq)]
struct ManagedWindowSnapshot {
id: WindowId,
workspace: u64,
app_id: String,
title: String,
state: ferese_shell_v1::WindowState,
}
#[derive(Clone, Debug, PartialEq)]
struct OutputSnapshot {
id: OutputId,
name: String,
active_workspace: WorkspaceId,
focused: bool,
}
#[derive(Clone, Debug, PartialEq)]
struct WorkspaceSnapshot {
id: WorkspaceId,
output: Option<OutputId>,
name: String,
active: bool,
}
fn split_id(id: u64) -> (u32, u32) {
((id >> 32) as u32, id as u32)
}
fn join_id(hi: u32, lo: u32) -> WindowId {
WindowId(join_u64(hi, lo))
}
fn join_u64(hi: u32, lo: u32) -> u64 {
(u64::from(hi) << 32) | u64::from(lo)
}
fn send_request_failed(shell: &FereseShellV1, request: ferese_shell_v1::FailedRequest, id: u64) {
let (object_hi, object_lo) = split_id(id);
shell.request_failed(
request,
object_hi,
object_lo,
"unknown or unavailable shell object".to_owned(),
);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn snapshot_deduplication_preserves_shell_visible_changes() {
let original = ShellSnapshot {
outputs: vec![OutputSnapshot {
id: OutputId(1),
name: "display".into(),
active_workspace: WorkspaceId(1),
focused: true,
}],
workspaces: vec![WorkspaceSnapshot {
id: WorkspaceId(1),
output: Some(OutputId(1)),
name: "1".into(),
active: true,
}],
windows: vec![ManagedWindowSnapshot {
id: WindowId(1),
workspace: 1,
app_id: "terminal".into(),
title: "shell".into(),
state: ferese_shell_v1::WindowState::Focused,
}],
};
assert_eq!(original, original.clone());
let changes: &[fn(&mut ShellSnapshot)] = &[
|s| s.outputs[0].focused = false,
|s| s.outputs[0].active_workspace = WorkspaceId(2),
|s| s.outputs.clear(),
|s| s.workspaces[0].output = None,
|s| s.workspaces[0].active = false,
|s| s.workspaces[0].name = "renamed".into(),
|s| s.windows[0].workspace = 2,
|s| s.windows[0].title = "new title".into(),
|s| s.windows[0].app_id = "new app".into(),
|s| s.windows[0].state |= ferese_shell_v1::WindowState::Fullscreen,
|s| s.windows.clear(),
];
for change in changes {
let mut updated = original.clone();
change(&mut updated);
assert_ne!(original, updated);
}
}
#[test]
fn config_chunks_are_bounded_and_preserve_unicode() {
let source = "abc🌲é".repeat(9000);
let chunks = config_chunks(&source);
assert!(
chunks
.iter()
.all(|chunk| !chunk.is_empty() && chunk.len() <= 1024)
);
assert_eq!(chunks.concat(), source);
assert!(config_chunks("").is_empty());
}
#[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));
}
}
}