// Copyright 2024 System76 // SPDX-License-Identifier: GPL-3.0-only use cosmic::cctk::cosmic_protocols::toplevel_info::v1::client::zcosmic_toplevel_handle_v1; use cosmic::cctk::cosmic_protocols::toplevel_management::v1::client::zcosmic_toplevel_manager_v1; use cosmic::cctk::cosmic_protocols::workspace::v2::client::zcosmic_workspace_handle_v2; use cosmic::cctk::wayland_client::protocol::{wl_output, wl_shm}; use cosmic::cctk::wayland_client::{Connection, WEnum}; use cosmic::cctk::wayland_protocols::ext::workspace::v1::client::ext_workspace_handle_v1; use cosmic::iced::futures::executor::block_on; use cosmic::iced::futures::{FutureExt, SinkExt}; use cosmic::iced::{self}; use cosmic::iced_winit::platform_specific::wayland::subsurface_widget::{Shmbuf, SubsurfaceBuffer}; use futures_channel::mpsc; use std::collections::HashSet; use std::io::{self, Write}; use std::sync::Arc; use std::sync::atomic::{AtomicU32, Ordering}; use std::{fs, thread}; use super::{CaptureImage, Cmd, Event}; use crate::utils; #[derive(Eq, PartialEq, Clone, Debug, Hash)] pub struct MockObjectId(u32); impl MockObjectId { pub fn protocol_id(&self) -> u32 { self.0 } } fn create_solid_capture_image(r: u8, g: u8, b: u8) -> CaptureImage { let file = fs::File::from(utils::create_memfile().unwrap()); let mut file = io::BufWriter::new(file); for i in 0..512 * 512 { file.write(&[b, g, r, 255]).unwrap(); } CaptureImage { width: 512, height: 512, wl_buffer: SubsurfaceBuffer::new(Arc::new( Shmbuf { fd: file.into_inner().unwrap().into(), offset: 0, width: 512, height: 512, stride: 512 * 4, format: wl_shm::Format::Argb8888, } .into(), )) .0, transform: wl_output::Transform::Normal, #[cfg(feature = "no-subsurfaces")] image: cosmic::widget::image::Handle::from_rgba(512, 512, [r, g, b, 255].repeat(512 * 512)), } } impl MockObjectId { fn new() -> Self { static NEXT_MOCK_ID: AtomicU32 = AtomicU32::new(0); Self(NEXT_MOCK_ID.fetch_add(1, Ordering::SeqCst)) } } #[derive(Eq, PartialEq, Clone, Debug, Hash)] pub struct ExtWorkspaceHandleV1(MockObjectId); impl ExtWorkspaceHandleV1 { pub fn id(&self) -> MockObjectId { self.0.clone() } } #[derive(Eq, PartialEq, Clone, Debug, Hash)] pub struct ExtForeignToplevelHandleV1(MockObjectId); #[derive(Clone, Debug, Default)] pub struct ToplevelInfo { pub title: String, pub app_id: String, pub state: HashSet, pub output: HashSet, pub workspace: HashSet, } #[derive(Clone, Debug)] pub struct Workspace { pub handle: ExtWorkspaceHandleV1, pub name: String, pub coordinates: Vec, pub state: ext_workspace_handle_v1::State, pub capabilities: ext_workspace_handle_v1::WorkspaceCapabilities, pub cosmic_capabilities: zcosmic_workspace_handle_v2::WorkspaceCapabilities, pub cosmic_state: zcosmic_workspace_handle_v2::State, // pub tiling: Option>, } pub fn subscription(conn: Connection) -> iced::Subscription { iced::Subscription::run_with_id("wayland-mock-sub", async { start(conn) }.flatten_stream()) } struct AppData { sender: mpsc::Sender, outputs: Vec, workspaces: Vec<(HashSet, Workspace)>, } impl AppData { fn send_event(&mut self, event: Event) { let _ = block_on(self.sender.send(event)); } fn add_output(&mut self, output: &wl_output::WlOutput) { // Add four workspaces for each output let mut new_workspaces = Vec::new(); for i in 0..=4 { let workspace_handle = ExtWorkspaceHandleV1(MockObjectId::new()); let workspace = Workspace { handle: workspace_handle.clone(), name: format!("Workspace {i}"), coordinates: vec![i], state: if i == 0 { ext_workspace_handle_v1::State::Active } else { ext_workspace_handle_v1::State::empty() }, capabilities: ext_workspace_handle_v1::WorkspaceCapabilities::Activate, cosmic_capabilities: zcosmic_workspace_handle_v2::WorkspaceCapabilities::empty(), cosmic_state: zcosmic_workspace_handle_v2::State::empty(), }; // Add three toplevels for each workspace for j in 0..=3 { let toplevel_handle = ExtForeignToplevelHandleV1(MockObjectId::new()); let toplevel_info = ToplevelInfo { title: format!("App {}", j), app_id: "com.example.app".to_string(), state: if i == 0 { HashSet::from([zcosmic_toplevel_handle_v1::State::Activated]) } else { HashSet::new() }, output: HashSet::from([output.clone()]), workspace: HashSet::from([workspace_handle.clone()]), }; self.send_event(Event::NewToplevel(toplevel_handle.clone(), toplevel_info)); self.send_event(Event::ToplevelCapture( toplevel_handle, create_solid_capture_image(255, 0, 0), )); } self.workspaces .push((HashSet::from([output.clone()]), workspace)); new_workspaces.push(workspace_handle); } self.send_event(Event::Workspaces(self.workspaces.clone())); for workspace_handle in new_workspaces { self.send_event(Event::WorkspaceCapture( workspace_handle, create_solid_capture_image(0, 255, 0), )); } self.outputs.push(output.clone()); } fn handle_cmd(&mut self, cmd: Cmd) { match cmd { Cmd::CaptureFilter(filter) => { for output in &filter.workspaces_on_outputs { if !self.outputs.contains(output) { self.add_output(output); } } } Cmd::ActivateToplevel(toplevel_handle) => { println!("Activate {:?}", toplevel_handle); } Cmd::CloseToplevel(toplevel_handle) => { println!("Close {:?}", toplevel_handle); } Cmd::MoveToplevelToWorkspace(toplevel_handle, workspace_handle, output) => {} Cmd::ActivateWorkspace(workspace_handle) => { println!("Activate {:?}", workspace_handle); } // TODO Cmd::MoveWorkspaceBefore(_, _) | Cmd::MoveWorkspaceAfter(_, _) | Cmd::SetWorkspacePinned(_, _) => {} } } } fn start(_conn: Connection) -> mpsc::Receiver { let (sender, receiver) = mpsc::channel(20); thread::spawn(move || { let mut event_loop = calloop::EventLoop::try_new().unwrap(); let (cmd_sender, cmd_channel) = calloop::channel::channel(); event_loop .handle() .insert_source(cmd_channel, |cmd, (), app_data: &mut AppData| match cmd { calloop::channel::Event::Msg(cmd) => app_data.handle_cmd(cmd), calloop::channel::Event::Closed => {} }) .unwrap(); let mut app_data = AppData { sender, outputs: Vec::new(), workspaces: Vec::new(), }; app_data.send_event(Event::ToplevelCapabilities(vec![ zcosmic_toplevel_manager_v1::ZcosmicToplelevelManagementCapabilitiesV1::Close, zcosmic_toplevel_manager_v1::ZcosmicToplelevelManagementCapabilitiesV1::Activate, zcosmic_toplevel_manager_v1::ZcosmicToplelevelManagementCapabilitiesV1::MoveToWorkspace, ])); app_data.send_event(Event::CmdSender(cmd_sender)); loop { event_loop.dispatch(None, &mut app_data).unwrap(); } }); receiver } // TODO WorkspaceCapture, ToplevelCapture, NewToplevel, Workspaces