From 970885ec45551e725d26ed32174be8dd5f013644 Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Fri, 30 Dec 2022 14:07:39 -0800 Subject: [PATCH] Initial commit --- Cargo.toml | 13 ++ src/main.rs | 261 +++++++++++++++++++++++++++++++++++++ src/wayland.rs | 344 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 618 insertions(+) create mode 100644 Cargo.toml create mode 100644 src/main.rs create mode 100644 src/wayland.rs diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..6994770 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "cosmic-workspaces" +version = "0.1.0" +edition = "2021" + +[dependencies] +cctk = { package = "cosmic-client-toolkit", git = "https://github.com/pop-os/cosmic-protocols" } +futures-channel = "0.3.25" +iced = { git = "https://github.com/pop-os/libcosmic", features = ["tokio"] } +iced_native = { git = "https://github.com/pop-os/libcosmic" } +iced_sctk = { git = "https://github.com/pop-os/libcosmic" } +libcosmic = { git = "https://github.com/pop-os/libcosmic" } +tokio = "1.23.0" diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..34df5ff --- /dev/null +++ b/src/main.rs @@ -0,0 +1,261 @@ +use cctk::{ + cosmic_protocols::workspace::v1::client::zcosmic_workspace_handle_v1, + sctk::shell::layer::{Anchor, KeyboardInteractivity, Layer}, + wayland_client::protocol::wl_output, +}; +use iced::{ + event::wayland::{Event as WaylandEvent, OutputEvent}, + keyboard::KeyCode, + sctk_settings::InitialSurface, + Application, Command, Element, Subscription, +}; +use iced_native::{ + command::platform_specific::wayland::layer_surface::{IcedOutput, SctkLayerSurfaceSettings}, + window::Id as SurfaceId, +}; +use iced_sctk::{ + application::SurfaceIdWrapper, + commands::layer_surface::{destroy_layer_surface, get_layer_surface}, +}; +use std::{collections::HashMap, process}; + +mod wayland; + +#[derive(Debug)] +enum Msg { + WaylandEvent(WaylandEvent), + Wayland(wayland::Event), + Close, + Closed(SurfaceIdWrapper), +} + +#[derive(Debug)] +struct Workspace { + name: String, + img: Option, + handle: zcosmic_workspace_handle_v1::ZcosmicWorkspaceHandleV1, + output: wl_output::WlOutput, +} + +struct LayerSurface { + output: wl_output::WlOutput, + //workspaces: Vec, + // Active workspace + // windows in workspace + // - for transitions, would need windows in more than one workspace +} + +#[derive(Default)] +struct App { + max_surface_id: usize, + layer_surfaces: HashMap, + workspaces: Vec, +} + +impl App { + fn next_surface_id(&mut self) -> SurfaceId { + self.max_surface_id += 1; + SurfaceId::new(self.max_surface_id) + } +} + +impl Application for App { + type Message = Msg; + type Theme = cosmic::Theme; + type Executor = iced::executor::Default; + type Flags = (); + + fn new(_flags: ()) -> (Self, Command) { + //(Self::default(), destroy_layer_surface(SurfaceId::new(0))) + (Self::default(), Command::none()) + } + + fn title(&self) -> String { + String::from("cosmic-workspaces") + } + + fn update(&mut self, message: Msg) -> Command { + match message { + Msg::WaylandEvent(evt) => match evt { + WaylandEvent::Output(evt, output) => match evt { + OutputEvent::Created(Some(info)) => { + //println!("Create: {:?}", output); + if let Some((width, height)) = info.logical_size { + let id = self.next_surface_id(); + self.layer_surfaces.insert( + id.clone(), + LayerSurface { + output: output.clone(), + //workspaces: Vec::new(), + }, + ); + // /* + return get_layer_surface(SctkLayerSurfaceSettings { + id, + keyboard_interactivity: KeyboardInteractivity::Exclusive, + //keyboard_interactivity: KeyboardInteractivity::None, + namespace: "workspaces".into(), + layer: Layer::Overlay, + size: Some((Some(width as _), Some(height as _))), + output: IcedOutput::Output(output), + ..Default::default() + }); + // */ + } + } + OutputEvent::Removed => { + if let Some((id, _)) = self + .layer_surfaces + .iter() + .find(|(_id, surface)| &surface.output == &output) + { + let id = *id; + self.layer_surfaces.remove(&id).unwrap(); + } + } + // TODO handle update/remove + _ => {} + }, + _ => {} + }, + Msg::Wayland(evt) => { + println!("{:?}", evt); + match evt { + wayland::Event::Workspaces(workspaces) => { + // XXX efficiency + // XXX removal + self.workspaces = Vec::new(); + for (output, workspace) in workspaces { + /* + if output != &surface.output { + continue; + } + */ + self.workspaces.push(Workspace { + name: workspace.name, + handle: workspace.handle, + output, + img: None, + }); + println!("add workspace"); + // Oh, set workspaces before surfaces created? + } + } + wayland::Event::WorkspaceCapture(workspace, image) => { + // XXX performanc + for i in &mut self.workspaces { + if &i.handle == &workspace { + i.img = Some(image.clone()); + } + } + } + } + } + Msg::Close => { + //println!("Close"); + std::process::exit(0); + } + Msg::Closed(_) => {} + } + + Command::none() + } + + fn subscription(&self) -> Subscription { + let events = iced::subscription::events_with(|evt, _| { + //println!("{:?}", evt); + if let iced::Event::PlatformSpecific(iced::event::PlatformSpecific::Wayland(evt)) = evt + { + Some(Msg::WaylandEvent(evt)) + } else if let iced::Event::Keyboard(iced::keyboard::Event::KeyReleased { + key_code: KeyCode::Escape, + modifiers: _, + }) = evt + { + Some(Msg::Close) + } else { + None + } + }); + iced::Subscription::batch(vec![events, wayland::subscription().map(Msg::Wayland)]) + } + + fn view(&self, id: SurfaceIdWrapper) -> cosmic::Element { + use iced::widget::*; + if let SurfaceIdWrapper::LayerSurface(id) = id { + if let Some(surface) = self.layer_surfaces.get(&id) { + return layer_surface(self, surface); + } + }; + text("workspaces").into() + } + + fn close_requested(&self, id: SurfaceIdWrapper) -> Msg { + Msg::Closed(id) + } +} + +fn layer_surface<'a>(app: &'a App, surface: &'a LayerSurface) -> cosmic::Element<'a, Msg> { + //workspaces_sidebar(app.workspaces.iter().filter(|i| &i.output == &surface.output)) + workspaces_sidebar(app.workspaces.iter()) +} + +fn workspace_sidebar_entry(workspace: &Workspace) -> cosmic::Element { + // x to close + // captured preview + // number name + // - selectable + iced::widget::column![ + iced::widget::Image::new( + workspace + .img + .clone() + .unwrap_or_else(|| iced::widget::image::Handle::from_pixels( + 0, + 0, + vec![0, 0, 0, 255] + )) + ), + iced::widget::text(&workspace.name) + ] + .height(iced::Length::Fill) + .width(iced::Length::Fill) + .into() +} + +fn workspaces_sidebar<'a>( + workspaces: impl Iterator, +) -> cosmic::Element<'a, Msg> { + //println!("{:?}", workspaces); + iced::widget::column(workspaces.map(workspace_sidebar_entry).collect()).into() + // New workspace +} + +/* +fn window_preview(&Window) -> cosmic::Element { + // capture of window + // - selectable + // name of window +} + +fn window_previews(windows: &[Window]) -> cosmic::Element { + iced::widgets::row(windows.iter().map(window_preview).collect()) +} +*/ + +// TODO create one surface per monitor? +// TODO how to get monitor size? +pub fn main() -> iced::Result { + App::run(iced::Settings { + antialiasing: true, + exit_on_close_request: false, + initial_surface: InitialSurface::LayerSurface(SctkLayerSurfaceSettings { + keyboard_interactivity: KeyboardInteractivity::None, + namespace: "ignore".into(), + size: Some((Some(1), Some(1))), + layer: Layer::Background, + ..Default::default() + }), + ..iced::Settings::default() + }) +} diff --git a/src/wayland.rs b/src/wayland.rs new file mode 100644 index 0000000..2185ba9 --- /dev/null +++ b/src/wayland.rs @@ -0,0 +1,344 @@ +// Workspaces Info, Toplevel Info +// Capture +// - subscribe to all workspaces, to start with? All that are associated with an output should be +// shown on one. +// * Need output name to compare? + +use cctk::{ + cosmic_protocols::{ + screencopy::v1::client::{zcosmic_screencopy_manager_v1, zcosmic_screencopy_session_v1}, + toplevel_info::v1::client::zcosmic_toplevel_handle_v1, + workspace::v1::client::zcosmic_workspace_handle_v1, + }, + screencopy::{BufferInfo, ScreencopyHandler, ScreencopyState}, + sctk::{ + self, + output::{OutputHandler, OutputState}, + registry::{ProvidesRegistryState, RegistryState}, + shm::{raw::RawPool, ShmHandler, ShmState}, + }, + toplevel_info::{ToplevelInfoHandler, ToplevelInfoState}, + wayland_client::{ + backend::ObjectId, + globals::registry_queue_init, + protocol::{wl_buffer, wl_output, wl_shm}, + Connection, Dispatch, Proxy, QueueHandle, WEnum, + }, + workspace::{WorkspaceHandler, WorkspaceState}, +}; +use futures_channel::mpsc; +use iced::{ + futures::{executor::block_on, FutureExt, SinkExt}, + widget::image, +}; +use std::{collections::HashMap, thread}; + +// TODO define subscription for a particular output/workspace/toplevel (but we want to rate limit?) + +#[derive(Debug)] +pub enum Event { + Workspaces(Vec<(wl_output::WlOutput, cctk::workspace::Workspace)>), + WorkspaceCapture( + zcosmic_workspace_handle_v1::ZcosmicWorkspaceHandleV1, + image::Handle, + ), +} + +pub fn subscription() -> iced::Subscription { + iced::subscription::run("wayland-sub", async { start() }.flatten_stream()) +} + +enum CaptureSource { + Workspace(zcosmic_workspace_handle_v1::ZcosmicWorkspaceHandleV1), +} + +struct Frame { + buffer: Option<(RawPool, wl_buffer::WlBuffer, BufferInfo)>, + source: CaptureSource, + first_frame: bool, +} + +struct AppData { + qh: QueueHandle, + output_state: OutputState, + registry_state: RegistryState, + toplevel_info_state: ToplevelInfoState, + workspace_state: WorkspaceState, + screencopy_state: ScreencopyState, + shm_state: ShmState, + sender: mpsc::Sender, + frames: HashMap, +} + +impl ProvidesRegistryState for AppData { + fn registry(&mut self) -> &mut RegistryState { + &mut self.registry_state + } + + sctk::registry_handlers!(OutputState,); +} + +impl ShmHandler for AppData { + fn shm_state(&mut self) -> &mut ShmState { + &mut self.shm_state + } +} + +// TODO: don't need this if we use same connection with same IDs? Or? +impl OutputHandler for AppData { + fn output_state(&mut self) -> &mut OutputState { + &mut self.output_state + } + + fn new_output( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _output: wl_output::WlOutput, + ) { + } + + fn update_output( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _output: wl_output::WlOutput, + ) { + } + + fn output_destroyed( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _output: wl_output::WlOutput, + ) { + } +} + +// TODO any indication when we have all toplevels? +impl ToplevelInfoHandler for AppData { + fn toplevel_info_state(&mut self) -> &mut ToplevelInfoState { + &mut self.toplevel_info_state + } + + fn new_toplevel( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + toplevel: &zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1, + ) { + /* + println!( + "New toplevel: {:?}", + self.toplevel_info_state.info(toplevel).unwrap() + ); + */ + } + + fn update_toplevel( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + toplevel: &zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1, + ) { + /* + println!( + "Update toplevel: {:?}", + self.toplevel_info_state.info(toplevel).unwrap() + ); + */ + } + + fn toplevel_closed( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + toplevel: &zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1, + ) { + /* + println!( + "Closed toplevel: {:?}", + self.toplevel_info_state.info(toplevel).unwrap() + ); + */ + } +} + +impl WorkspaceHandler for AppData { + fn workspace_state(&mut self) -> &mut WorkspaceState { + &mut self.workspace_state + } + + fn done(&mut self) { + let mut workspaces = Vec::new(); + + for group in self.workspace_state.workspace_groups() { + /* + println!( + "Group: capabilities: {:?}, output: {:?}", + &group.capabilities, &group.output + ); + */ + for workspace in &group.workspaces { + //println!("{:?}", &workspace); + + if let Some(output) = group.output.as_ref() { + workspaces.push((output.clone(), workspace.clone())); + + //println!("capture workspace"); + let frame = self.screencopy_state.screencopy_manager.capture_workspace( + &workspace.handle, + output, + zcosmic_screencopy_manager_v1::CursorMode::Hidden, + &self.qh, + Default::default(), // TODO + ); + // XXX first_frame + self.frames.insert( + frame.id(), + Frame { + buffer: None, + source: CaptureSource::Workspace(workspace.handle.clone()), + first_frame: false, + }, + ); + } + } + } + + let _ = block_on(self.sender.send(Event::Workspaces(workspaces))); + } +} + +impl ScreencopyHandler for AppData { + fn screencopy_state(&mut self) -> &mut ScreencopyState { + &mut self.screencopy_state + } + + fn init_done( + &mut self, + conn: &Connection, + qh: &QueueHandle, + session: &zcosmic_screencopy_session_v1::ZcosmicScreencopySessionV1, + buffer_infos: &[BufferInfo], + ) { + //println!("init_done"); + // TODO BIND + + // XXX + let buffer_info = buffer_infos + .iter() + .find(|x| { + x.type_ == WEnum::Value(zcosmic_screencopy_session_v1::BufferType::WlShm) + && x.format == wl_shm::Format::Abgr8888.into() + }) + .unwrap(); + let buf_len = buffer_info.stride * buffer_info.height; + + let mut pool = RawPool::new(buf_len as usize, &self.shm_state).unwrap(); + let buffer = pool.create_buffer( + 0, + buffer_info.width as i32, + buffer_info.height as i32, + buffer_info.stride as i32, + wl_shm::Format::Abgr8888, + (), + qh, + ); + + let mut frame = self.frames.get_mut(&session.id()).unwrap(); + + session.attach_buffer(&buffer, None, 0); // XXX age? + if frame.first_frame { + session.commit(zcosmic_screencopy_session_v1::Options::empty()); + } else { + session.commit(zcosmic_screencopy_session_v1::Options::OnDamage); + } + conn.flush().unwrap(); + + frame.buffer = Some((pool, buffer, buffer_info.clone())); + } + + fn ready( + &mut self, + conn: &Connection, + qh: &QueueHandle, + session: &zcosmic_screencopy_session_v1::ZcosmicScreencopySessionV1, + ) { + let frame = self.frames.get_mut(&session.id()).unwrap(); + let (mut pool, buffer, buffer_info) = frame.buffer.take().unwrap(); + match &frame.source { + CaptureSource::Workspace(workspace) => { + let image = image::Handle::from_pixels( + buffer_info.width, + buffer_info.height, + pool.mmap().to_vec(), + ); // XXX + let _ = block_on( + self.sender + .send(Event::WorkspaceCapture(workspace.clone(), image)), + ); + } + } + //println!("ready"); + // TODO + } + + fn failed( + &mut self, + conn: &Connection, + qh: &QueueHandle, + session: &zcosmic_screencopy_session_v1::ZcosmicScreencopySessionV1, + reason: WEnum, + ) { + //println!("failed"); + // TODO + } +} + +impl Dispatch for AppData { + fn event( + _app_data: &mut Self, + buffer: &wl_buffer::WlBuffer, + event: wl_buffer::Event, + _: &(), + _: &Connection, + _qh: &QueueHandle, + ) { + } +} + +fn start() -> mpsc::Receiver { + let (sender, receiver) = mpsc::channel(20); + + // TODO share connection? Can't use same `WlOutput` with seperate connection + let conn = Connection::connect_to_env().unwrap(); + let (globals, mut event_queue) = registry_queue_init(&conn).unwrap(); + let qh = event_queue.handle(); + + let registry_state = RegistryState::new(&globals); + let mut app_data = AppData { + qh: qh.clone(), + output_state: OutputState::new(&globals, &qh), + workspace_state: WorkspaceState::new(®istry_state, &qh), // Create before toplevel info state + toplevel_info_state: ToplevelInfoState::new(®istry_state, &qh), + screencopy_state: ScreencopyState::new(&globals, &qh), + registry_state, + shm_state: ShmState::bind(&globals, &qh).unwrap(), + sender, + frames: HashMap::new(), + }; + + thread::spawn(move || loop { + event_queue.blocking_dispatch(&mut app_data).unwrap(); + }); + + receiver +} + +sctk::delegate_output!(AppData); +sctk::delegate_registry!(AppData); +sctk::delegate_shm!(AppData); +cctk::delegate_toplevel_info!(AppData); +cctk::delegate_workspace!(AppData); +cctk::delegate_screencopy!(AppData);