diff --git a/Cargo.lock b/Cargo.lock index 48655d91..13723c03 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -277,8 +277,10 @@ name = "cosmic-app-list" version = "0.1.0" dependencies = [ "anyhow", + "calloop", "cascade", "cosmic-panel-config", + "cosmic-protocols", "futures", "futures-util", "gio 0.16.0", @@ -288,13 +290,17 @@ dependencies = [ "i18n-embed", "i18n-embed-fl", "libcosmic", + "log", + "nix 0.24.1", "once_cell", "pretty_env_logger", "relm4-macros", + "ron", "rust-embed", "serde", "serde_json", - "tokio", + "wayland-backend", + "wayland-client 0.30.0-beta.8", "xdg", ] @@ -442,8 +448,7 @@ dependencies = [ "rust-embed", "tokio", "wayland-backend", - "wayland-client 0.30.0-beta.7", - "wayland-commons", + "wayland-client 0.30.0-beta.8", ] [[package]] @@ -493,13 +498,13 @@ dependencies = [ [[package]] name = "cosmic-protocols" version = "0.1.0" -source = "git+https://github.com/pop-os/cosmic-protocols#1962ffdca3d9c914929eea358ebeab61ff2217a8" +source = "git+https://github.com/pop-os/cosmic-protocols#81d6a50bdc91af5968f87785fc19a16cf261c96b" dependencies = [ "bitflags", "wayland-backend", - "wayland-client 0.30.0-beta.7", - "wayland-protocols 0.30.0-beta.7", - "wayland-scanner 0.30.0-beta.7", + "wayland-client 0.30.0-beta.8", + "wayland-protocols 0.30.0-beta.8", + "wayland-scanner 0.30.0-beta.8", ] [[package]] @@ -2768,17 +2773,16 @@ checksum = "6a89911bd99e5f3659ec4acf9c4d93b0a90fe4a2a11f15328472058edc5261be" [[package]] name = "wayland-backend" -version = "0.1.0-beta.7" +version = "0.1.0-beta.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a861eb7cd51f67de60f228a570f142396d94759babcb427f861071ffb0757c9e" +checksum = "0ee8e77c63b0cdc68bfc7b407b862b0fe2718949ce060b32d4f94ef1ea9607a4" dependencies = [ "cc", "downcast-rs", - "log", "nix 0.24.1", "scoped-tls", "smallvec", - "wayland-sys 0.30.0-beta.7", + "wayland-sys 0.30.0-beta.8", ] [[package]] @@ -2798,18 +2802,17 @@ dependencies = [ [[package]] name = "wayland-client" -version = "0.30.0-beta.7" +version = "0.30.0-beta.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dca5290499da69c21fcf64b4021886963511b888af056dbfb6bebfb7e1587e6" +checksum = "0f9e0d862c23f07b2c4b49de66b0680948af5dd1d2def17f1ddc16520352bf14" dependencies = [ "bitflags", "futures-channel", "futures-core", - "log", "nix 0.24.1", "thiserror", "wayland-backend", - "wayland-scanner 0.30.0-beta.7", + "wayland-scanner 0.30.0-beta.8", ] [[package]] @@ -2838,14 +2841,14 @@ dependencies = [ [[package]] name = "wayland-protocols" -version = "0.30.0-beta.7" +version = "0.30.0-beta.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d64adbf2e145b9da69ff0e9bb72fc513182978c826fc6f704c05f0f80b663a6d" +checksum = "e47c45a60d531d5a513601f47f51a4743901836778ddae208ae9124606be1719" dependencies = [ "bitflags", "wayland-backend", - "wayland-client 0.30.0-beta.7", - "wayland-scanner 0.30.0-beta.7", + "wayland-client 0.30.0-beta.8", + "wayland-scanner 0.30.0-beta.8", ] [[package]] @@ -2861,9 +2864,9 @@ dependencies = [ [[package]] name = "wayland-scanner" -version = "0.30.0-beta.7" +version = "0.30.0-beta.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3baff545c2f5a0c32d796595d0b3c8fafccf29e72e557ff1969fe552ff093d6" +checksum = "87933ccc3df4f6335cf240aca0647aa34319fdd693dda503f645ca4df4e10386" dependencies = [ "proc-macro2", "quote", @@ -2882,9 +2885,9 @@ dependencies = [ [[package]] name = "wayland-sys" -version = "0.30.0-beta.7" +version = "0.30.0-beta.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f62b62672d36b6cf2f7d936f95c9f5894c0609190fa789c2ce46b73912baf239" +checksum = "beca223ed017df1b356ff181d4d6e7f2b135418c4888df5bb02df7a563f02ab0" dependencies = [ "dlib", "log", diff --git a/applets/cosmic-app-list/Cargo.toml b/applets/cosmic-app-list/Cargo.toml index 10dfc0d4..80ef2052 100644 --- a/applets/cosmic-app-list/Cargo.toml +++ b/applets/cosmic-app-list/Cargo.toml @@ -6,24 +6,31 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] cosmic-panel-config = {git = "https://github.com/pop-os/cosmic-panel", features = ["gtk4"] } +cosmic-protocols = { git = "https://github.com/pop-os/cosmic-protocols", default-features = false, features = ["client"] } cascade = "1.0.0" gtk4 = { git = "https://github.com/gtk-rs/gtk4-rs", features = ["v4_4"] } gio = { git = "https://github.com/gtk-rs/gtk-rs-core" } libcosmic = { git = "https://github.com/pop-os/libcosmic", branch = "relm4-next" } relm4-macros = { git = "https://github.com/Relm4/Relm4.git", branch = "next" } -serde = "1.0.136" serde_json = "1.0.78" -tokio = { version = "1.16.1", features = ["sync"] } futures = "0.3.19" futures-util = "0.3.19" once_cell = "1.9.0" xdg = "2.4.0" gsk4 = { git = "https://github.com/gtk-rs/gtk4-rs" } pretty_env_logger = "0.4" -anyhow = "1.0.50" i18n-embed = { version = "0.13.4", features = ["fluent-system", "desktop-requester"] } i18n-embed-fl = "0.6.4" rust-embed = "6.3.0" +calloop = "0.10.1" +wayland-backend = { version = "0.1.0-beta.7" } +wayland-client = { version = "0.30.0-beta.7" } +nix = "0.24.1" +# config +anyhow = "1.0.53" +ron = "0.7.0" +serde = { version = "1.0.136", features = ["derive"] } +log = "0.4" [build-dependencies] glib-build-tools = { git = "https://github.com/gtk-rs/gtk-rs-core" } diff --git a/applets/cosmic-app-list/data/com.system76.CosmicAppList.desktop b/applets/cosmic-app-list/data/com.system76.CosmicAppList.desktop index 369c6c69..0e47f470 100644 --- a/applets/cosmic-app-list/data/com.system76.CosmicAppList.desktop +++ b/applets/cosmic-app-list/data/com.system76.CosmicAppList.desktop @@ -10,3 +10,4 @@ Keywords=Gnome;GTK; Icon=com.system76.CosmicAppList.svg StartupNotify=true NoDisplay=true +HostWaylandDisplay=true \ No newline at end of file diff --git a/applets/cosmic-app-list/src/apps_container/mod.rs b/applets/cosmic-app-list/src/apps_container/mod.rs index b60a0ac0..64625077 100644 --- a/applets/cosmic-app-list/src/apps_container/mod.rs +++ b/applets/cosmic-app-list/src/apps_container/mod.rs @@ -1,16 +1,13 @@ -use std::env; - // SPDX-License-Identifier: MPL-2.0-only use crate::dock_list::DockList; use crate::dock_list::DockListType; -use crate::utils::Event; use cascade::cascade; -use cosmic_panel_config::{PanelAnchor, CosmicPanelConfig}; +use cosmic_panel_config::{CosmicPanelConfig, PanelAnchor}; use gtk4::prelude::*; use gtk4::subclass::prelude::*; use gtk4::Orientation; +use gtk4::Separator; use gtk4::{gio, glib}; -use tokio::sync::mpsc::Sender; mod imp; @@ -21,7 +18,7 @@ glib::wrapper! { } impl AppsContainer { - pub fn new(tx: Sender) -> Self { + pub fn new() -> Self { let self_: Self = glib::Object::new(&[]).expect("Failed to create AppsContainer"); let imp = imp::AppsContainer::from_instance(&self_); @@ -34,25 +31,25 @@ impl AppsContainer { let config = CosmicPanelConfig::load_from_env().unwrap_or_default(); - let saved_app_list_view = DockList::new(DockListType::Saved, tx.clone(), config.clone()); + let saved_app_list_view = DockList::new(DockListType::Saved, config.clone()); self_.append(&saved_app_list_view); - // let separator_container = cascade! { - // gtk4::Box::new(Orientation::Vertical, 0); - // ..set_margin_top(8); - // ..set_margin_bottom(8); - // ..set_vexpand(true); - // }; - // self_.append(&separator_container); - // let separator = cascade! { - // Separator::new(Orientation::Vertical); - // ..set_margin_start(8); - // ..set_margin_end(8); - // ..set_vexpand(true); - // ..add_css_class("dock_separator"); - // }; - // separator_container.append(&separator); - let active_app_list_view = DockList::new(DockListType::Active, tx, config.clone()); + let separator_container = cascade! { + gtk4::Box::new(Orientation::Vertical, 0); + ..set_margin_top(8); + ..set_margin_bottom(8); + ..set_vexpand(true); + }; + self_.append(&separator_container); + let separator = cascade! { + Separator::new(Orientation::Vertical); + ..set_margin_start(8); + ..set_margin_end(8); + ..set_vexpand(true); + ..add_css_class("dock_separator"); + }; + separator_container.append(&separator); + let active_app_list_view = DockList::new(DockListType::Active, config.clone()); self_.append(&active_app_list_view); // self_.connect_orientation_notify(glib::clone!(@weak separator => move |c| { // dbg!(c.orientation()); @@ -67,13 +64,12 @@ impl AppsContainer { // Setup self_.setup_callbacks(); self_.set_position(config.anchor); - Self::setup_callbacks(&self_); self_ } - + pub fn model(&self, type_: DockListType) -> &gio::ListStore { // Get state let imp = imp::AppsContainer::from_instance(self); diff --git a/applets/cosmic-app-list/src/apps_window/mod.rs b/applets/cosmic-app-list/src/apps_window/mod.rs index 2f454905..0db1afe0 100644 --- a/applets/cosmic-app-list/src/apps_window/mod.rs +++ b/applets/cosmic-app-list/src/apps_window/mod.rs @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MPL-2.0-only -use crate::{apps_container::AppsContainer, fl, Event}; +use crate::{apps_container::AppsContainer, fl, AppListEvent}; use cascade::cascade; use gtk4::{ gio, @@ -8,7 +8,6 @@ use gtk4::{ prelude::*, subclass::prelude::*, }; -use tokio::sync::mpsc; mod imp; @@ -20,7 +19,7 @@ glib::wrapper! { } impl CosmicAppListWindow { - pub fn new(app: >k4::Application, tx: mpsc::Sender) -> Self { + pub fn new(app: >k4::Application) -> Self { let self_: Self = Object::new(&[("application", app)]).expect("Failed to create `CosmicAppListWindow`."); let imp = imp::CosmicAppListWindow::from_instance(&self_); @@ -34,7 +33,7 @@ impl CosmicAppListWindow { ..set_title(Some(&fl!("cosmic-app-list"))); ..add_css_class("transparent"); }; - let app_list = AppsContainer::new(tx); + let app_list = AppsContainer::new(); self_.set_child(Some(&app_list)); imp.inner.set(app_list).unwrap(); @@ -43,6 +42,13 @@ impl CosmicAppListWindow { self_ } + pub fn apps_container(&self) -> &AppsContainer { + imp::CosmicAppListWindow::from_instance(&self) + .inner + .get() + .unwrap() + } + fn setup_shortcuts(&self) { let window = self.clone().upcast::(); let action_quit = gio::SimpleAction::new("quit", None); diff --git a/applets/cosmic-app-list/src/config.rs b/applets/cosmic-app-list/src/config.rs new file mode 100644 index 00000000..f255159d --- /dev/null +++ b/applets/cosmic-app-list/src/config.rs @@ -0,0 +1,36 @@ +use crate::ID; +use anyhow::anyhow; +use serde::Deserialize; +use std::fmt::Debug; +use std::fs::File; +use xdg::BaseDirectories; + +#[derive(Debug, Clone, Deserialize)] +pub enum TopLevelFilter { + ActiveWorkspace, + ConfiguredOutput, +} + +#[derive(Debug, Clone, Default, Deserialize)] +pub struct AppListConfig { + pub filter_top_levels: Option, +} + +impl AppListConfig { + /// load config with the provided name + pub fn load() -> anyhow::Result { + let file = match BaseDirectories::new() + .ok() + .and_then(|dirs| dirs.find_config_file(format!("{ID}/config.ron"))) + .and_then(|p| File::open(p).ok()) + { + Some(path) => path, + _ => { + anyhow::bail!("Failed to load config"); + } + }; + + ron::de::from_reader::<_, AppListConfig>(file) + .map_err(|err| anyhow!("Failed to parse config file: {}", err)) + } +} diff --git a/applets/cosmic-app-list/src/dock_item/imp.rs b/applets/cosmic-app-list/src/dock_item/imp.rs index cc8a7991..13d44855 100644 --- a/applets/cosmic-app-list/src/dock_item/imp.rs +++ b/applets/cosmic-app-list/src/dock_item/imp.rs @@ -1,17 +1,15 @@ // SPDX-License-Identifier: MPL-2.0-only -use glib::subclass::Signal; -use gtk4::glib; -use gtk4::prelude::*; -use gtk4::subclass::prelude::*; +use gtk4::{ + glib::{self, subclass::Signal}, + prelude::*, + subclass::prelude::*, +}; use once_cell::sync::Lazy; -use once_cell::sync::OnceCell; use std::cell::{Cell, RefCell}; use std::rc::Rc; -use tokio::sync::mpsc::Sender; use crate::dock_popover::DockPopover; -use crate::utils::Event; #[derive(Debug, Default)] pub struct DockItem { @@ -20,7 +18,6 @@ pub struct DockItem { pub item_box: Rc>, pub popover: Rc>, pub popover_menu: Rc>>, - pub tx: OnceCell>, pub icon_size: Rc>, } diff --git a/applets/cosmic-app-list/src/dock_item/mod.rs b/applets/cosmic-app-list/src/dock_item/mod.rs index 046e6956..a4c66b82 100644 --- a/applets/cosmic-app-list/src/dock_item/mod.rs +++ b/applets/cosmic-app-list/src/dock_item/mod.rs @@ -2,8 +2,8 @@ use crate::dock_object::DockObject; use crate::dock_popover::DockPopover; +use crate::utils::AppListEvent; use crate::utils::BoxedWindowList; -use crate::utils::Event; use cascade::cascade; use cosmic_panel_config::PanelAnchor; use gtk4::glib; @@ -14,7 +14,6 @@ use gtk4::Image; use gtk4::Orientation; use gtk4::Popover; use gtk4::{Align, PositionType}; -use tokio::sync::mpsc::Sender; mod imp; @@ -25,7 +24,7 @@ glib::wrapper! { } impl DockItem { - pub fn new(tx: Sender, icon_size: u32) -> Self { + pub fn new(icon_size: u32) -> Self { let self_: DockItem = glib::Object::new(&[]).expect("Failed to create DockItem"); let item_box = Box::new(Orientation::Vertical, 0); @@ -50,7 +49,7 @@ impl DockItem { ..set_valign(Align::Center); ..add_css_class("transparent"); }; - // TODO dots inverse color of parent with gsk blend modes? + item_box.append(&image); item_box.append(&dots); let popover = cascade! { @@ -66,7 +65,7 @@ impl DockItem { }); let popover_menu = cascade! { - DockPopover::new(tx.clone()); + DockPopover::new(); ..add_css_class("popover_menu"); }; popover.set_child(Some(&popover_menu)); @@ -87,7 +86,6 @@ impl DockItem { imp.item_box.replace(item_box); imp.popover.replace(popover); imp.popover_menu.replace(Some(popover_menu)); - imp.tx.set(tx).unwrap(); self_ } @@ -112,6 +110,7 @@ impl DockItem { while let Some(c) = dots.first_child() { dots.remove(&c); } + for _ in active.0 { dots.append(&cascade! { Box::new(Orientation::Horizontal, 0); diff --git a/applets/cosmic-app-list/src/dock_list/imp.rs b/applets/cosmic-app-list/src/dock_list/imp.rs index 7f8603e6..d11e6dce 100644 --- a/applets/cosmic-app-list/src/dock_list/imp.rs +++ b/applets/cosmic-app-list/src/dock_list/imp.rs @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MPL-2.0-only -use cosmic_panel_config::{PanelAnchor, CosmicPanelConfig}; +use cosmic_panel_config::{CosmicPanelConfig, PanelAnchor}; use glib::SignalHandlerId; use gtk4::subclass::prelude::*; use gtk4::{gio, glib}; @@ -8,9 +8,8 @@ use gtk4::{Box, DragSource, DropTarget, GestureClick, ListView}; use once_cell::sync::OnceCell; use std::cell::{Cell, RefCell}; use std::rc::Rc; -use tokio::sync::mpsc; -use crate::utils::Event; +use crate::utils::AppListEvent; #[derive(Debug, Default)] pub struct DockList { @@ -24,8 +23,7 @@ pub struct DockList { pub drag_cancel_signal: Rc>>, pub popover_menu_index: Rc>>, pub position: Rc>, - pub tx: OnceCell>, - pub config: OnceCell + pub config: OnceCell, } #[glib::object_subclass] diff --git a/applets/cosmic-app-list/src/dock_list/mod.rs b/applets/cosmic-app-list/src/dock_list/mod.rs index dda1d6e7..2348b598 100644 --- a/applets/cosmic-app-list/src/dock_list/mod.rs +++ b/applets/cosmic-app-list/src/dock_list/mod.rs @@ -1,32 +1,26 @@ // SPDX-License-Identifier: MPL-2.0-only -use crate::dock_item::DockItem; -use crate::dock_object::DockObject; -use crate::utils::data_path; -use crate::utils::{BoxedWindowList, Event, Item}; +use crate::{ + dock_item::DockItem, + utils::{AppListEvent, BoxedWindowList, data_path}, + wayland::{Toplevel, ToplevelEvent}, + {TX, WAYLAND_TX}, dock_object::DockObject, +}; use cascade::cascade; -use gio::DesktopAppInfo; -use gio::Icon; -use glib::Object; -use glib::Type; -use gtk4::gdk; -use gtk4::gdk::ContentProvider; -use gtk4::gdk::Display; -use gtk4::gdk::ModifierType; -use gtk4::glib; -use gtk4::prelude::ListModelExt; -use gtk4::prelude::*; -use gtk4::subclass::prelude::*; -use gtk4::DropTarget; -use gtk4::IconTheme; -use gtk4::ListView; -use gtk4::Orientation; -use gtk4::SignalListItemFactory; -use gtk4::{DragSource, GestureClick}; -use std::fs::File; -use std::path::Path; use cosmic_panel_config::{CosmicPanelConfig, PanelAnchor}; -use tokio::sync::mpsc::Sender; + +use gio::traits::AppLaunchContextExt; +use gtk4::{ + gdk::{self, ContentProvider, Display, ModifierType}, + gio::{self, DesktopAppInfo, Icon}, + glib::{self, Object, Type}, + prelude::ListModelExt, + prelude::*, + subclass::prelude::*, + DropTarget, IconTheme, ListView, Orientation, SignalListItemFactory, + {DragSource, GestureClick}, +}; +use std::{fs::File, path::Path}; mod imp; @@ -49,11 +43,10 @@ impl Default for DockListType { } impl DockList { - pub fn new(type_: DockListType, tx: Sender, config: CosmicPanelConfig) -> Self { + pub fn new(type_: DockListType, config: CosmicPanelConfig) -> Self { let self_: DockList = glib::Object::new(&[]).expect("Failed to create DockList"); let imp = imp::DockList::from_instance(&self_); imp.type_.set(type_).unwrap(); - imp.tx.set(tx).unwrap(); imp.config.set(config).unwrap(); self_.layout(); //dnd behavior is different for each type, as well as the data in the model @@ -84,6 +77,7 @@ impl DockList { } fn restore_data(&self) { + // TODO use IDs instead of names if let Ok(file) = File::open(data_path()) { if let Ok(data) = serde_json::from_reader::<_, Vec>(file) { // dbg!(&data); @@ -219,7 +213,6 @@ impl DockList { let model = self.model(); let list_view = &imp.list_view.get().unwrap(); let popover_menu_index = &imp.popover_menu_index; - let tx = imp.tx.get().unwrap().clone(); controller.connect_released(glib::clone!(@weak model, @weak list_view, @weak popover_menu_index => move |self_, _, x, y| { let max_x = list_view.allocated_width(); let max_y = list_view.allocated_height(); @@ -238,14 +231,14 @@ impl DockList { // dbg!(click_modifier); // Launch the application when an item of the list is activated - let tx = tx.clone(); - let focus_window = move |first_focused_item: &Item| { - let entity = first_focused_item.entity; - let tx = tx.clone(); - glib::MainContext::default().spawn_local(async move { - let _ = tx.clone().send(Event::Activate(entity)).await; - }); + // TODO use seat eventually + // let wl_seat = self_.device().map(|d| d.seat().downcast::().unwrap().wl_seat()).unwrap(); + let focus_window = move |first_focused_item: &Toplevel| { + let toplevel_handle = first_focused_item.toplevel_handle.clone(); + let tx = WAYLAND_TX.get().unwrap().clone(); + let _ = tx.clone().send(ToplevelEvent::Activate(toplevel_handle)); }; + let old_index = popover_menu_index.get(); if let Some(old_index) = old_index { if let Some(old_item) = model.item(old_index) { @@ -322,7 +315,6 @@ impl DockList { let list_view = &imp.list_view.get().unwrap(); let drag_end = &imp.drag_end_signal; let drag_source = &imp.drag_source.get().unwrap(); - let tx = imp.tx.get().unwrap().clone(); drop_controller.connect_drop( glib::clone!(@weak model, @weak list_view, @weak drag_end, @weak drag_source => @default-return true, move |_self, drop_value, x, y| { //calculate insertion location @@ -388,10 +380,8 @@ impl DockList { // dbg!("rejecting drop"); _self.reject(); } - let tx = tx.clone(); - glib::MainContext::default().spawn_local(async move { - let _ = tx.send(Event::RefreshFromCache).await; - }); + let tx = TX.get().unwrap().clone(); + let _ = tx.send(AppListEvent::Refresh); true }), ); @@ -419,7 +409,6 @@ impl DockList { let drag_end = &imp.drag_end_signal; let drag_cancel = &imp.drag_cancel_signal; let type_ = *type_; - let tx = imp.tx.get().unwrap().clone(); list_view.add_controller(&drag_source); drag_source.connect_prepare(glib::clone!(@weak model, @weak list_view, @weak drag_end, @weak drag_cancel => @default-return None, move |self_, x, _y| { let max_x = list_view.allocated_width(); @@ -430,30 +419,25 @@ impl DockList { let index = (x * n_buckets as f64 / (max_x as f64 + 0.1)) as u32; if let Some(item) = model.item(index) { if type_ == DockListType::Saved { - let tx1 = tx.clone(); if let Some(old_handle) = drag_end.replace(Some(self_.connect_drag_end( glib::clone!(@weak model => move |_self, _drag, _delete_data| { if _delete_data { model.remove(index); - let tx = tx1.clone(); - glib::MainContext::default().spawn_local(async move { - let _ = tx.send(Event::RefreshFromCache).await; - }); + let tx = TX.get().unwrap().clone(); + let _ = tx.send(AppListEvent::Refresh); + }; }), ))) { glib::signal_handler_disconnect(self_, old_handle); } - let tx = tx.clone(); if let Some(old_handle) = drag_cancel.replace(Some(self_.connect_drag_cancel( glib::clone!(@weak model => @default-return false, move |_self, _drag, cancel_reason| { if cancel_reason != gdk::DragCancelReason::UserCancelled { model.remove(index); - let tx = tx.clone(); - glib::MainContext::default().spawn_local(async move { - let _ = tx.send(Event::RefreshFromCache).await; - }); + let tx = TX.get().unwrap().clone(); + let _ = tx.send(AppListEvent::Refresh); true } else { false @@ -515,11 +499,10 @@ impl DockList { let popover_menu_index = &imp.popover_menu_index; let factory = SignalListItemFactory::new(); let model = imp.model.get().expect("Failed to get saved app model."); - let tx = imp.tx.get().unwrap().clone(); let icon_size = imp.config.get().unwrap().get_applet_icon_size(); factory.connect_setup( glib::clone!(@weak popover_menu_index, @weak model => move |_, list_item| { - let dock_item = DockItem::new(tx.clone(), icon_size); + let dock_item = DockItem::new(icon_size); dock_item .connect_local("popover-closed", false, move |_| { if let Some(old_index) = popover_menu_index.replace(None) { diff --git a/applets/cosmic-app-list/src/dock_object/mod.rs b/applets/cosmic-app-list/src/dock_object/mod.rs index f484477d..df0974ed 100644 --- a/applets/cosmic-app-list/src/dock_object/mod.rs +++ b/applets/cosmic-app-list/src/dock_object/mod.rs @@ -78,9 +78,9 @@ impl DockObject { imp.saved.replace(is_saved); } - pub fn from_search_results(results: BoxedWindowList) -> Self { - let appinfo = if let Some(first) = results.0.get(0) { - xdg::BaseDirectories::new() + pub fn from_window_list(results: BoxedWindowList) -> Option { + if let Some(first) = results.0.get(0) { + return xdg::BaseDirectories::new() .expect("could not access XDG Base directory") .get_data_dirs() .iter_mut() @@ -95,9 +95,23 @@ impl DockObject { if let Some(path) = path.to_str() { if let Some(app_info) = gio::DesktopAppInfo::new(path) { if app_info.should_show() - && first.description.as_str() == app_info.name().as_str() + && Some(&first.app_id) + == app_info + .filename() + .and_then(|p| { + p.file_stem().and_then(|s| { + s.to_str().map(|s| s.to_string()) + }) + }) + .as_ref() { - return Some(app_info); + return Some( + Object::new(&[ + ("appinfo", &app_info), + ("active", &results), + ]) + .expect("Failed to create `DockObject`."), + ); } } } @@ -105,13 +119,9 @@ impl DockObject { } None }) - .next() - } else { - None - }; - // dbg!(&appinfo); - Object::new(&[("appinfo", &appinfo), ("active", &results)]) - .expect("Failed to create `DockObject`.") + .next(); + } + None } pub fn set_popover(&self, b: bool) { diff --git a/applets/cosmic-app-list/src/dock_popover/imp.rs b/applets/cosmic-app-list/src/dock_popover/imp.rs index 08ee90f3..212963d3 100644 --- a/applets/cosmic-app-list/src/dock_popover/imp.rs +++ b/applets/cosmic-app-list/src/dock_popover/imp.rs @@ -9,11 +9,8 @@ use gtk4::prelude::*; use gtk4::subclass::prelude::*; use gtk4::{Box, Button, ListBox, Revealer}; use once_cell::sync::Lazy; -use once_cell::sync::OnceCell; -use tokio::sync::mpsc::Sender; use crate::dock_object::DockObject; -use crate::utils::Event; #[derive(Debug, Default)] pub struct DockPopover { @@ -26,7 +23,6 @@ pub struct DockPopover { pub quit_all_item: Rc>, //TODO figure out how to use lifetimes with glib::wrapper! macro pub dock_object: Rc>>, - pub tx: OnceCell>, } #[glib::object_subclass] diff --git a/applets/cosmic-app-list/src/dock_popover/mod.rs b/applets/cosmic-app-list/src/dock_popover/mod.rs index a10d51fa..8f0b73be 100644 --- a/applets/cosmic-app-list/src/dock_popover/mod.rs +++ b/applets/cosmic-app-list/src/dock_popover/mod.rs @@ -7,11 +7,12 @@ use gtk4::subclass::prelude::*; use gtk4::{gdk, gio, glib}; use gtk4::{prelude::*, Label}; use gtk4::{Box, Button, Image, ListBox, Orientation}; -use tokio::sync::mpsc::Sender; use crate::dock_object::DockObject; +use crate::utils::AppListEvent; use crate::utils::BoxedWindowList; -use crate::utils::Event; +use crate::wayland::ToplevelEvent; +use crate::{TX, WAYLAND_TX}; mod imp; @@ -22,10 +23,9 @@ glib::wrapper! { } impl DockPopover { - pub fn new(tx: Sender) -> Self { + pub fn new() -> Self { let self_: DockPopover = glib::Object::new(&[]).expect("Failed to create DockList"); let imp = imp::DockPopover::from_instance(&self_); - imp.tx.set(tx).unwrap(); self_.layout(); //dnd behavior is different for each type, as well as the data in the model self_ @@ -85,12 +85,11 @@ impl DockPopover { ..add_css_class("title-4"); ..add_css_class("dock_popover_title"); }; - - let window_image = cascade! { - //TODO fill with image of window - Image::from_pixbuf(None); - }; - window_box.append(&window_image); + //TODO fill with image of window + // let window_image = cascade! { + // Image::from_pixbuf(None); + // }; + // window_box.append(&window_image); window_box.append(&window_title); } // imp.all_windows_item_revealer.replace(window_list_revealer); @@ -192,30 +191,24 @@ impl DockPopover { self_.emit_hide(); })); - let tx = imp.tx.get().unwrap().clone(); let self_ = self.clone(); quit_all_item.connect_clicked(glib::clone!(@weak dock_object => move |_| { let active = dock_object.property::("active").0; for w in active { - let entity = w.entity; - let tx = tx.clone(); - glib::MainContext::default().spawn_local(async move { - let _ = tx.clone().send(Event::Close(entity)).await; - }); + let t = w.toplevel_handle.clone(); + let tx = WAYLAND_TX.get().unwrap().clone(); + let _ = tx.clone().send(ToplevelEvent::Close(t)); } self_.emit_hide(); })); - let tx = imp.tx.get().unwrap().clone(); let self_ = self.clone(); favorite_item.connect_clicked(glib::clone!(@weak dock_object => move |_| { let saved = dock_object.property::("saved"); - let tx = tx.clone(); - glib::MainContext::default().spawn_local(async move { - if let Some(name) = dock_object.get_name() { - let _ = tx.clone().send(Event::Favorite((name, !saved))).await; - } - }); + if let Some(name) = dock_object.get_name() { + let tx = TX.get().unwrap().clone(); + let _ = tx.clone().send(AppListEvent::Favorite((name, !saved))); + } self_.emit_hide(); })); @@ -227,16 +220,13 @@ impl DockPopover { // }), // ); - let tx = imp.tx.get().unwrap().clone(); let self_ = self.clone(); window_listbox.connect_row_activated( glib::clone!(@weak dock_object => move |_, item| { let active = dock_object.property::("active").0; - let entity = active[usize::try_from(item.index()).unwrap()].entity; - let tx = tx.clone(); - glib::MainContext::default().spawn_local(async move { - let _ = tx.send(Event::Activate(entity)).await; - }); + let t = active[usize::try_from(item.index()).unwrap()].toplevel_handle.clone(); + let tx = WAYLAND_TX.get().unwrap().clone(); + let _ = tx.send(ToplevelEvent::Activate(t)); self_.emit_hide(); }), ); diff --git a/applets/cosmic-app-list/src/main.rs b/applets/cosmic-app-list/src/main.rs index 3da8b078..db4e5818 100644 --- a/applets/cosmic-app-list/src/main.rs +++ b/applets/cosmic-app-list/src/main.rs @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MPL-2.0-only use apps_window::CosmicAppListWindow; +use calloop::channel::SyncSender; use dock_list::DockListType; use dock_object::DockObject; use gio::{ApplicationFlags, DesktopAppInfo}; @@ -8,22 +9,24 @@ use gtk4::gdk::Display; use gtk4::{glib, prelude::*, CssProvider, StyleContext}; use once_cell::sync::OnceCell; use std::collections::BTreeMap; -use std::sync::{Arc, Mutex}; -use std::time::Duration; -use tokio::sync::mpsc; -use utils::{block_on, BoxedWindowList, Event, Item, DEST, PATH}; +use utils::{block_on, AppListEvent, BoxedWindowList, DEST, PATH}; +use wayland::{Toplevel, ToplevelEvent}; mod apps_container; mod apps_window; +mod config; mod dock_item; mod dock_list; mod dock_object; mod dock_popover; mod localize; mod utils; +mod wayland; +mod wayland_source; const ID: &str = "com.system76.CosmicAppList"; -static TX: OnceCell> = OnceCell::new(); +static TX: OnceCell> = OnceCell::new(); +static WAYLAND_TX: OnceCell> = OnceCell::new(); pub fn localize() { let localizer = crate::localize::localizer(); @@ -57,202 +60,222 @@ fn main() { app.connect_activate(|app| { load_css(); - let (tx, mut rx) = mpsc::channel(100); + let (tx, rx) = glib::MainContext::channel(glib::Priority::default()); - let window = CosmicAppListWindow::new(app, tx.clone()); + let window = CosmicAppListWindow::new(app); + let wayland_tx = wayland::spawn_toplevels(); - let apps_container = apps_container::AppsContainer::new(tx.clone()); - let cached_results = Arc::new(Mutex::new(Vec::new())); + WAYLAND_TX.set(wayland_tx).unwrap(); + + + + let mut cached_results = Vec::new(); // let zbus_conn = spawn_zbus(tx.clone(), Arc::clone(&cached_results)); TX.set(tx.clone()).unwrap(); - let _ = glib::MainContext::default().spawn_local(async move { - while let Some(event) = rx.recv().await { - match event { - Event::Activate(_) => { - // let _activate_window = zbus_conn - // .call_method(Some(DEST), PATH, Some(DEST), "WindowFocus", &((e,))) - // .await - // .expect("Failed to focus selected window"); - } - Event::Close(_) => { - // let _activate_window = zbus_conn - // .call_method(Some(DEST), PATH, Some(DEST), "WindowQuit", &((e,))) - // .await - // .expect("Failed to close selected window"); - } - Event::Favorite((name, should_favorite)) => { - let saved_app_model = apps_container.model(DockListType::Saved); - let active_app_model = apps_container.model(DockListType::Active); - if should_favorite { - let mut cur: u32 = 0; - let mut index: Option = None; - while let Some(item) = active_app_model.item(cur) { - if let Ok(cur_dock_object) = item.downcast::() { - if cur_dock_object.get_path() == Some(name.clone()) { - cur_dock_object.set_saved(true); - index = Some(cur); - } + rx.attach(None, glib::clone!(@weak window => @default-return glib::prelude::Continue(true), move |event| { + let apps_container = window.apps_container(); + let should_apply_changes = match event { + AppListEvent::Favorite((name, should_favorite)) => { + let saved_app_model = apps_container.model(DockListType::Saved); + let active_app_model = apps_container.model(DockListType::Active); + if should_favorite { + let mut cur: u32 = 0; + let mut index: Option = None; + while let Some(item) = active_app_model.item(cur) { + if let Ok(cur_dock_object) = item.downcast::() { + if cur_dock_object.get_name() == Some(name.clone()) { + cur_dock_object.set_saved(true); + index = Some(cur); } - cur += 1; - } - if let Some(index) = index { - let object = active_app_model.item(index).unwrap(); - active_app_model.remove(index); - saved_app_model.append(&object); - } - } else { - let mut cur: u32 = 0; - let mut index: Option = None; - while let Some(item) = saved_app_model.item(cur) { - if let Ok(cur_dock_object) = item.downcast::() { - if cur_dock_object.get_path() == Some(name.clone()) { - cur_dock_object.set_saved(false); - index = Some(cur); - } - } - cur += 1; - } - if let Some(index) = index { - let object = saved_app_model.item(index).unwrap(); - saved_app_model.remove(index); - active_app_model.append(&object); } + cur += 1; } - let _ = tx.send(Event::RefreshFromCache).await; - } - Event::RefreshFromCache => { - // println!("refreshing model from cache"); - let cached_results = cached_results.as_ref().lock().unwrap(); - let stack_active = cached_results.iter().fold( - BTreeMap::new(), - |mut acc: BTreeMap, elem: &Item| { - if let Some(v) = acc.get_mut(&elem.description) { - v.0.push(elem.clone()); - } else { - acc.insert( - elem.description.clone(), - BoxedWindowList(vec![elem.clone()]), - ); - } - acc - }, - ); - let mut stack_active: Vec = - stack_active.into_values().collect(); - - // update active app stacks for saved apps into the saved app model - // then put the rest in the active app model (which doesn't include saved apps) - let saved_app_model = apps_container.model(DockListType::Saved); - - let mut saved_i: u32 = 0; - while let Some(item) = saved_app_model.item(saved_i) { - if let Ok(dock_obj) = item.downcast::() { - if let Some(cur_app_info) = - dock_obj.property::>("appinfo") - { - if let Some((i, _s)) = stack_active - .iter() - .enumerate() - .find(|(_i, s)| s.0[0].description == cur_app_info.name()) - { - // println!( - // "found active saved app {} at {}", - // _s.0[0].name, i - // ); - let active = stack_active.remove(i); - dock_obj.set_property("active", active.to_value()); - saved_app_model.items_changed(saved_i, 0, 0); - } else if cached_results - .iter() - .any(|s| s.description == cur_app_info.name()) - { - dock_obj.set_property( - "active", - BoxedWindowList(Vec::new()).to_value(), - ); - saved_app_model.items_changed(saved_i, 0, 0); - } + if let Some(index) = index { + let object = active_app_model.item(index).unwrap(); + active_app_model.remove(index); + saved_app_model.append(&object); + } + } else { + let mut cur: u32 = 0; + let mut index: Option = None; + while let Some(item) = saved_app_model.item(cur) { + if let Ok(cur_dock_object) = item.downcast::() { + if cur_dock_object.get_name() == Some(name.clone()) { + cur_dock_object.set_saved(false); + index = Some(cur); } } - saved_i += 1; + cur += 1; } - - let active_app_model = apps_container.model(DockListType::Active); - let model_len = active_app_model.n_items(); - let new_results: Vec = stack_active - .into_iter() - .map(|v| DockObject::from_search_results(v).upcast()) - .collect(); - active_app_model.splice(0, model_len, &new_results[..]); - } - Event::WindowList => { - // sort to make comparison with cache easier - let results = cached_results.as_ref().lock().unwrap(); - - // build active app stacks for each app - let stack_active = results.iter().fold( - BTreeMap::new(), - |mut acc: BTreeMap, elem| { - if let Some(v) = acc.get_mut(&elem.description) { - v.0.push(elem.clone()); - } else { - acc.insert( - elem.description.clone(), - BoxedWindowList(vec![elem.clone()]), - ); - } - acc - }, - ); - let mut stack_active: Vec = - stack_active.into_values().collect(); - - // update active app stacks for saved apps into the saved app model - // then put the rest in the active app model (which doesn't include saved apps) - let saved_app_model = apps_container.model(DockListType::Saved); - - let mut saved_i: u32 = 0; - while let Some(item) = saved_app_model.item(saved_i) { - if let Ok(dock_obj) = item.downcast::() { - if let Some(cur_app_info) = - dock_obj.property::>("appinfo") - { - if let Some((i, _s)) = stack_active - .iter() - .enumerate() - .find(|(_i, s)| s.0[0].description == cur_app_info.name()) - { - // println!("found active saved app {} at {}", s.0[0].name, i); - let active = stack_active.remove(i); - dock_obj.set_property("active", active.to_value()); - saved_app_model.items_changed(saved_i, 0, 0); - } else if results - .iter() - .any(|s| s.description == cur_app_info.name()) - { - dock_obj.set_property( - "active", - BoxedWindowList(Vec::new()).to_value(), - ); - saved_app_model.items_changed(saved_i, 0, 0); - } - } - } - saved_i += 1; + if let Some(index) = index { + let object = saved_app_model.item(index).unwrap(); + saved_app_model.remove(index); + active_app_model.append(&object); } - - let active_app_model = apps_container.model(DockListType::Active); - let model_len = active_app_model.n_items(); - let new_results: Vec = stack_active - .into_iter() - .map(|v| DockObject::from_search_results(v).upcast()) - .collect(); - active_app_model.splice(0, model_len, &new_results[..]); } + let _ = tx.send(AppListEvent::Refresh); + false } + AppListEvent::Refresh => { + // println!("refreshing model from cache"); + let stack_active = cached_results.iter().fold( + BTreeMap::new(), + |mut acc: BTreeMap, elem: &Toplevel| { + if let Some(v) = acc.get_mut(&elem.app_id) { + v.0.push(elem.clone()); + } else { + acc.insert( + elem.app_id.clone(), + BoxedWindowList(vec![elem.clone()]), + ); + } + acc + }, + ); + let mut stack_active: Vec = + stack_active.into_values().collect(); + + // update active app stacks for saved apps into the saved app model + // then put the rest in the active app model (which doesn't include saved apps) + let saved_app_model = apps_container.model(DockListType::Saved); + + let mut saved_i: u32 = 0; + while let Some(item) = saved_app_model.item(saved_i) { + if let Ok(dock_obj) = item.downcast::() { + if let Some(cur_app_info) = + dock_obj.property::>("appinfo") + { + if let Some((i, _s)) = stack_active + .iter() + .enumerate() + .find(|(_i, s)| Some(&s.0[0].app_id) == cur_app_info.filename().and_then(|p| p + .file_stem() + .and_then(|s| s.to_str().map(|s| s.to_string()))).as_ref()) + { + // println!( + // "found active saved app {} at {}", + // _s.0[0].name, i + // ); + let active = stack_active.remove(i); + dock_obj.set_property("active", active.to_value()); + saved_app_model.items_changed(saved_i, 0, 0); + } else if cached_results + .iter() + .any(|s| Some(&s.app_id) == cur_app_info.filename().and_then(|p| p + .file_stem() + .and_then(|s| s.to_str().map(|s| s.to_string()))).as_ref()) + { + dock_obj.set_property( + "active", + BoxedWindowList(Vec::new()).to_value(), + ); + saved_app_model.items_changed(saved_i, 0, 0); + } + } + } + saved_i += 1; + } + + let active_app_model = apps_container.model(DockListType::Active); + let model_len = active_app_model.n_items(); + let new_results: Vec = stack_active + .into_iter() + .filter_map(|v| DockObject::from_window_list(v).map(|o| o.upcast())) + .collect(); + active_app_model.splice(0, model_len, &new_results[..]); + true + } + AppListEvent::WindowList(toplevels) => { + cached_results = toplevels; + true + } + AppListEvent::Remove(top_level) => { + if let Some(i) = cached_results.iter().position(|t| t.toplevel_handle == top_level.toplevel_handle) { + cached_results.swap_remove(i); + } + true + } + AppListEvent::Add(top_level) => { + // sort to make comparison with cache easier + if let Some(i) = cached_results.iter().position(|t| t.toplevel_handle == top_level.toplevel_handle) { + cached_results[i] = top_level; + } else { + cached_results.push(top_level); + } + true + } + }; + if should_apply_changes { + // dbg!(&cached_results); + // build active app stacks for each app + let stack_active = cached_results.iter().fold( + BTreeMap::new(), + |mut acc: BTreeMap, elem| { + if let Some(v) = acc.get_mut(&elem.app_id) { + v.0.push(elem.clone()); + } else { + acc.insert( + elem.app_id.clone(), + BoxedWindowList(vec![elem.clone()]), + ); + } + acc + }, + ); + let mut stack_active: Vec = + stack_active.into_values().collect(); + + // update active app stacks for saved apps into the saved app model + // then put the rest in the active app model (which doesn't include saved apps) + let saved_app_model = apps_container.model(DockListType::Saved); + + let mut saved_i: u32 = 0; + while let Some(item) = saved_app_model.item(saved_i) { + if let Ok(dock_obj) = item.downcast::() { + if let Some(cur_app_info) = + dock_obj.property::>("appinfo") + { + if let Some((i, _s)) = stack_active + .iter() + .enumerate() + .find(|(_i, s)| Some(&s.0[0].app_id) == cur_app_info.filename().and_then(|p| p + .file_stem() + .and_then(|s| s.to_str().map(|s| s.to_string()))).as_ref()) + { + // println!("found active saved app {} at {}", s.0[0].name, i); + let active = stack_active.remove(i); + dock_obj.set_property("active", active.to_value()); + saved_app_model.items_changed(saved_i, 0, 0); + } else if cached_results + .iter() + .any(|s| Some(&s.app_id) == cur_app_info.filename().and_then(|p| p + .file_stem() + .and_then(|s| s.to_str().map(|s| s.to_string()))).as_ref()) + { + dock_obj.set_property( + "active", + BoxedWindowList(Vec::new()).to_value(), + ); + saved_app_model.items_changed(saved_i, 0, 0); + } + } + } + saved_i += 1; + } + + let active_app_model = apps_container.model(DockListType::Active); + let model_len = active_app_model.n_items(); + let new_results: Vec = stack_active + .into_iter() + .filter_map(|v| DockObject::from_window_list(v).map(|o| o.upcast())) + .collect(); + + active_app_model.splice(0, model_len, &new_results[..]); } - }); + glib::prelude::Continue(true) + })); + window.show(); }); app.run(); diff --git a/applets/cosmic-app-list/src/style.css b/applets/cosmic-app-list/src/style.css index e4a9c1a7..3a2fbac3 100644 --- a/applets/cosmic-app-list/src/style.css +++ b/applets/cosmic-app-list/src/style.css @@ -48,6 +48,10 @@ button.dock_item { outline-color: transparent; } +label.dock_popover_title { + color: black; +} + button.dock_item:hover { border-radius: 12px; transition: 100ms; @@ -57,6 +61,12 @@ button.dock_item:hover { background: rgba(255, 255, 255, 0.1); } +box.dock_dots { + background: white; + padding: 2px; + border-radius: 4px; +} + *.transparent { border-color: transparent; background: transparent; diff --git a/applets/cosmic-app-list/src/utils.rs b/applets/cosmic-app-list/src/utils.rs index 868ccc37..7a6268c8 100644 --- a/applets/cosmic-app-list/src/utils.rs +++ b/applets/cosmic-app-list/src/utils.rs @@ -3,32 +3,25 @@ use std::path::PathBuf; use gtk4::glib; -use serde::{Deserialize, Serialize}; use std::future::Future; +use crate::wayland::Toplevel; + pub const DEST: &str = "com.System76.PopShell"; pub const PATH: &str = "/com/System76/PopShell"; #[derive(Debug)] -pub enum Event { - WindowList, - Activate((u32, u32)), - Close((u32, u32)), +pub enum AppListEvent { + WindowList(Vec), + Add(Toplevel), + Remove(Toplevel), Favorite((String, bool)), - RefreshFromCache, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Item { - pub(crate) entity: (u32, u32), - pub(crate) name: String, - pub(crate) description: String, - pub(crate) desktop_entry: String, + Refresh, } #[derive(Clone, Debug, Default, glib::Boxed)] #[boxed_type(name = "BoxedWindowList")] -pub struct BoxedWindowList(pub Vec); +pub struct BoxedWindowList(pub Vec); pub fn data_path() -> PathBuf { let mut path = glib::user_data_dir(); diff --git a/applets/cosmic-app-list/src/wayland.rs b/applets/cosmic-app-list/src/wayland.rs new file mode 100644 index 00000000..88b5d3e2 --- /dev/null +++ b/applets/cosmic-app-list/src/wayland.rs @@ -0,0 +1,599 @@ +use crate::config::AppListConfig; +use crate::{config::TopLevelFilter, utils::AppListEvent, wayland_source::WaylandSource, TX}; +use calloop::channel::*; +use cosmic_panel_config::CosmicPanelConfig; +use cosmic_protocols::{ + toplevel_info::v1::client::{ + zcosmic_toplevel_handle_v1::{self, ZcosmicToplevelHandleV1}, + zcosmic_toplevel_info_v1::{self, ZcosmicToplevelInfoV1}, + }, + toplevel_management::v1::client::zcosmic_toplevel_manager_v1::{ + self, ZcosmicToplevelManagerV1, + }, + workspace::v1::client::{ + zcosmic_workspace_group_handle_v1::{self, ZcosmicWorkspaceGroupHandleV1}, + zcosmic_workspace_handle_v1::{self, ZcosmicWorkspaceHandleV1}, + zcosmic_workspace_manager_v1::{self, ZcosmicWorkspaceManagerV1}, + }, +}; +use wayland_client::protocol::wl_seat::{WlSeat, self}; +use std::{env, os::unix::net::UnixStream, path::PathBuf, time::Duration}; +use wayland_client::{ + event_created_child, + protocol::{ + wl_output::{self, WlOutput}, + wl_registry, + }, + ConnectError, Proxy, +}; +use wayland_client::{Connection, Dispatch, QueueHandle}; + +#[derive(Debug, Clone)] +pub enum ToplevelEvent { + Activate(ZcosmicToplevelHandleV1), + Close(ZcosmicToplevelHandleV1), +} + +pub fn spawn_toplevels() -> SyncSender { + let config = AppListConfig::load().unwrap_or_default(); + + let (workspaces_tx, workspaces_rx) = calloop::channel::sync_channel(100); + + if let Ok(Ok(conn)) = std::env::var("WAYLAND_DISPLAY") + .map_err(anyhow::Error::msg) + .map(|display_str| { + let mut socket_path = env::var_os("XDG_RUNTIME_DIR") + .map(Into::::into) + .ok_or(ConnectError::NoCompositor)?; + socket_path.push(display_str); + + Ok(UnixStream::connect(socket_path).map_err(|_| ConnectError::NoCompositor)?) + }) + .and_then(|s| s.map(|s| Connection::from_socket(s).map_err(anyhow::Error::msg))) + { + std::thread::spawn(move || { + let output = match config.filter_top_levels { + Some(TopLevelFilter::ConfiguredOutput) => { + CosmicPanelConfig::load_from_env().ok().map(|c| c.output) + } + _ => None, + }; + let mut event_loop = calloop::EventLoop::::try_new().unwrap(); + let loop_handle = event_loop.handle(); + let event_queue = conn.new_event_queue::(); + let qhandle = event_queue.handle(); + + WaylandSource::new(event_queue) + .expect("Failed to create wayland source") + .insert(loop_handle) + .unwrap(); + + let display = conn.display(); + display.get_registry(&qhandle, ()).unwrap(); + + let mut state = State { + workspace_manager: None, + workspace_groups: Vec::new(), + toplevel_info: None, + toplevel_manager: None, + config, + configured_output: output, + expected_output: None, + running: true, + toplevels: vec![], + seats: vec![], + }; + let loop_handle = event_loop.handle(); + loop_handle + .insert_source(workspaces_rx, |e, _, state| match e { + Event::Msg(ToplevelEvent::Activate(toplevel)) => { + if let Some(manager) = &state.toplevel_manager { + for seat in &state.seats { + manager.activate(&toplevel,seat) + } } + } + Event::Msg(ToplevelEvent::Close(t)) => { + if let Some(manager) = &state.toplevel_manager { + manager.close(&t); + } + } + Event::Closed => { + if let Some(workspace_manager) = &mut state.workspace_manager { + for g in &mut state.workspace_groups { + g.workspace_group_handle.destroy(); + } + workspace_manager.stop(); + } + if let Some(toplevel_manager) = &mut state.toplevel_manager { + toplevel_manager.destroy(); + } + if let Some(toplevel_info) = &mut state.toplevel_info { + for toplevel in &state.toplevels { + toplevel.toplevel_handle.destroy(); + } + toplevel_info.stop(); + } + } + }) + .unwrap(); + while state.running { + event_loop + .dispatch(Duration::from_millis(16), &mut state) + .unwrap(); + } + }); + } else { + eprintln!("ENV variable WAYLAND_DISPLAY is missing. Exiting..."); + std::process::exit(1); + } + + workspaces_tx +} + +#[derive(Debug, Clone)] +pub struct State { + running: bool, + config: AppListConfig, + configured_output: Option, + expected_output: Option, + workspace_manager: Option, + workspace_groups: Vec, + toplevel_info: Option, + toplevel_manager: Option, + toplevels: Vec, + seats: Vec, +} + +impl State { + pub fn workspace_list(&self) -> impl Iterator + '_ { + self.workspace_groups + .iter() + .filter_map(|g| { + if g.output == self.expected_output { + Some(g.workspaces.iter().map(|w| { + ( + w.name.clone(), + match &w.states { + x if x.contains(&zcosmic_workspace_handle_v1::State::Active) => 0, + x if x.contains(&zcosmic_workspace_handle_v1::State::Urgent) => 1, + x if x.contains(&zcosmic_workspace_handle_v1::State::Hidden) => 2, + _ => 3, + }, + ) + })) + } else { + None + } + }) + .flatten() + } +} + +#[derive(Debug, Clone)] +pub struct Toplevel { + pub name: String, + pub app_id: String, + pub toplevel_handle: ZcosmicToplevelHandleV1, + pub states: Vec, + pub output: Option, + pub workspace: Option, +} + +#[derive(Debug, Clone)] +struct WorkspaceGroup { + workspace_group_handle: ZcosmicWorkspaceGroupHandleV1, + output: Option, + workspaces: Vec, +} + +#[derive(Debug, Clone)] +struct Workspace { + workspace_handle: ZcosmicWorkspaceHandleV1, + name: String, + coordinates: Vec, + states: Vec, +} + +impl Dispatch for State { + fn event( + state: &mut Self, + registry: &wl_registry::WlRegistry, + event: wl_registry::Event, + _: &(), + _: &Connection, + qh: &QueueHandle, + ) { + if let wl_registry::Event::Global { + name, + interface, + version, + } = event + { + match &interface[..] { + "zcosmic_toplevel_info_v1" => { + let ti = registry + .bind::(name, 1, qh, ()) + .unwrap(); + state.toplevel_info = Some(ti); + } + "zcosmic_toplevel_manager_v1" => { + let tm = registry + .bind::(name, 1, qh, ()) + .unwrap(); + state.toplevel_manager = Some(tm); + } + "zcosmic_workspace_manager_v1" => { + let workspace_manager = registry + .bind::(name, 1, qh, ()) + .unwrap(); + state.workspace_manager = Some(workspace_manager); + } + "wl_seat" => { + registry.bind::(name, 1, qh, ()).unwrap(); + } + "wl_output" => { + registry.bind::(name, 1, qh, ()).unwrap(); + } + _ => {} + } + } + } +} + +impl Dispatch for State { + fn event( + state: &mut Self, + _: &ZcosmicToplevelInfoV1, + event: ::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + match event { + zcosmic_toplevel_info_v1::Event::Toplevel { toplevel } => { + state.toplevels.push(Toplevel { + name: "".into(), + app_id: "".into(), + toplevel_handle: toplevel, + states: vec![], + output: None, + workspace: None, + }); + } + zcosmic_toplevel_info_v1::Event::Finished => { + todo!() + } + _ => {} + } + } + + event_created_child!(State, ZcosmicWorkspaceManagerV1, [ + 0 => (ZcosmicToplevelHandleV1, ()) + ]); +} + +impl Dispatch for State { + fn event( + _: &mut Self, + _: &ZcosmicToplevelManagerV1, + event: ::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + match event { + zcosmic_toplevel_manager_v1::Event::Capabilities { .. } => { + // TODO capabilities affect what is shown to user in applet + } + _ => {} + } + } +} + +impl Dispatch for State { + fn event( + state: &mut Self, + p: &ZcosmicToplevelHandleV1, + event: ::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + match event { + zcosmic_toplevel_handle_v1::Event::Closed => { + if let Some(i) = state.toplevels.iter().position(|t| &t.toplevel_handle == p) { + state.toplevels.remove(i); + } + } + zcosmic_toplevel_handle_v1::Event::Done => { + let to_send = match state.config.filter_top_levels { + Some(TopLevelFilter::ActiveWorkspace) => state.toplevels.iter_mut().find(|t| { + if &t.toplevel_handle == p { + state + .workspace_groups + .iter() + .find(|g| { + g.workspaces + .iter() + .find(|w| { + w.states.contains( + &zcosmic_workspace_handle_v1::State::Active, + ) && Some(&w.workspace_handle) == t.workspace.as_ref() + }) + .is_some() + }) + .is_some() + } else { + false + } + }), + Some(TopLevelFilter::ConfiguredOutput) => state + .toplevels + .iter_mut() + .find(|t| &t.toplevel_handle == p && state.expected_output == t.output), + _ => state.toplevels.iter_mut().find(|t| &t.toplevel_handle == p), + }; + + if let Some(toplevel) = to_send.cloned() { + let tx = TX.get().unwrap().clone(); + + let _ = tx.send(AppListEvent::Add(toplevel)); + } + } + zcosmic_toplevel_handle_v1::Event::Title { title } => { + if let Some(i) = state.toplevels.iter_mut().find(|t| &t.toplevel_handle == p) { + i.name = title; + } + } + zcosmic_toplevel_handle_v1::Event::AppId { app_id } => { + if let Some(i) = state.toplevels.iter_mut().find(|t| &t.toplevel_handle == p) { + i.app_id = app_id; + } + } + zcosmic_toplevel_handle_v1::Event::OutputEnter { output } => { + if let Some(i) = state.toplevels.iter_mut().find(|t| &t.toplevel_handle == p) { + i.output.replace(output); + } + } + zcosmic_toplevel_handle_v1::Event::OutputLeave { output } => { + if let Some(i) = state + .toplevels + .iter_mut() + .find(|t| &t.toplevel_handle == p && t.output.as_ref() == Some(&output)) + { + i.output.take(); + } + } + zcosmic_toplevel_handle_v1::Event::WorkspaceEnter { workspace } => { + if let Some(i) = state.toplevels.iter_mut().find(|t| &t.toplevel_handle == p) { + i.workspace.replace(workspace); + } + } + zcosmic_toplevel_handle_v1::Event::WorkspaceLeave { workspace } => { + if let Some(i) = state + .toplevels + .iter_mut() + .find(|t| &t.toplevel_handle == p && t.workspace.as_ref() == Some(&workspace)) + { + i.workspace.take(); + } + } + zcosmic_toplevel_handle_v1::Event::State { state: t_state } => { + if let Some(i) = state.toplevels.iter_mut().find(|t| &t.toplevel_handle == p) { + i.states = t_state + .chunks(4) + .map(|chunk| { + zcosmic_toplevel_handle_v1::State::try_from(u32::from_ne_bytes( + chunk.try_into().unwrap(), + )) + .unwrap() + }) + .collect(); + } + } + _ => todo!(), + } + } +} + +impl Dispatch for State { + fn event( + state: &mut Self, + _: &ZcosmicWorkspaceManagerV1, + event: zcosmic_workspace_manager_v1::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + match event { + zcosmic_workspace_manager_v1::Event::WorkspaceGroup { workspace_group } => { + state.workspace_groups.push(WorkspaceGroup { + workspace_group_handle: workspace_group, + output: None, + workspaces: Vec::new(), + }); + } + zcosmic_workspace_manager_v1::Event::Done => { + for group in &mut state.workspace_groups { + group.workspaces.sort_by(|w1, w2| { + w1.coordinates + .iter() + .zip(w2.coordinates.iter()) + .skip_while(|(coord1, coord2)| coord1 == coord2) + .next() + .map(|(coord1, coord2)| coord1.cmp(coord2)) + .unwrap_or(std::cmp::Ordering::Equal) + }); + } + } + zcosmic_workspace_manager_v1::Event::Finished => { + state.workspace_manager.take(); + } + _ => {} + } + } + + event_created_child!(State, ZcosmicWorkspaceManagerV1, [ + 0 => (ZcosmicWorkspaceGroupHandleV1, ()) + ]); +} + +impl Dispatch for State { + fn event( + state: &mut Self, + group: &ZcosmicWorkspaceGroupHandleV1, + event: zcosmic_workspace_group_handle_v1::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + match event { + zcosmic_workspace_group_handle_v1::Event::OutputEnter { output } => { + if let Some(group) = state + .workspace_groups + .iter_mut() + .find(|g| &g.workspace_group_handle == group) + { + group.output = Some(output); + } + } + zcosmic_workspace_group_handle_v1::Event::OutputLeave { output } => { + if let Some(group) = state.workspace_groups.iter_mut().find(|g| { + &g.workspace_group_handle == group && g.output.as_ref() == Some(&output) + }) { + group.output = None; + } + } + zcosmic_workspace_group_handle_v1::Event::Workspace { workspace } => { + if let Some(group) = state + .workspace_groups + .iter_mut() + .find(|g| &g.workspace_group_handle == group) + { + group.workspaces.push(Workspace { + workspace_handle: workspace, + name: String::new(), + coordinates: Vec::new(), + states: Vec::new(), + }) + } + } + zcosmic_workspace_group_handle_v1::Event::Remove => { + if let Some(group) = state + .workspace_groups + .iter() + .position(|g| &g.workspace_group_handle == group) + { + state.workspace_groups.remove(group); + } + } + _ => {} + } + } + + event_created_child!(State, ZcosmicWorkspaceGroupHandleV1, [ + 3 => (ZcosmicWorkspaceHandleV1, ()) + ]); +} + +impl Dispatch for State { + fn event( + state: &mut Self, + workspace: &ZcosmicWorkspaceHandleV1, + event: zcosmic_workspace_handle_v1::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + match event { + zcosmic_workspace_handle_v1::Event::Name { name } => { + if let Some(w) = state.workspace_groups.iter_mut().find_map(|g| { + g.workspaces + .iter_mut() + .find(|w| &w.workspace_handle == workspace) + }) { + w.name = name; + } + } + zcosmic_workspace_handle_v1::Event::Coordinates { coordinates } => { + if let Some(w) = state.workspace_groups.iter_mut().find_map(|g| { + g.workspaces + .iter_mut() + .find(|w| &w.workspace_handle == workspace) + }) { + // wayland is host byte order + w.coordinates = coordinates + .chunks(4) + .map(|chunk| u32::from_ne_bytes(chunk.try_into().unwrap())) + .collect(); + } + } + zcosmic_workspace_handle_v1::Event::State { + state: workspace_state, + } => { + if let Some(w) = state.workspace_groups.iter_mut().find_map(|g| { + g.workspaces + .iter_mut() + .find(|w| &w.workspace_handle == workspace) + }) { + // wayland is host byte order + w.states = workspace_state + .chunks(4) + .map(|chunk| { + zcosmic_workspace_handle_v1::State::try_from(u32::from_ne_bytes( + chunk.try_into().unwrap(), + )) + .unwrap() + }) + .collect(); + // TODO if workspace active status changes while configured to only show active workspace, clear the list + } + } + zcosmic_workspace_handle_v1::Event::Remove => { + if let Some((g, w_i)) = state.workspace_groups.iter_mut().find_map(|g| { + g.workspaces + .iter_mut() + .position(|w| &w.workspace_handle == workspace) + .map(|p| (g, p)) + }) { + g.workspaces.remove(w_i); + } + } + _ => {} + } + } +} + +impl Dispatch for State { + fn event( + state: &mut Self, + o: &WlOutput, + e: wl_output::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + match e { + wl_output::Event::Name { name } if Some(&name) == state.configured_output.as_ref() => { + state.expected_output.replace(o.clone()); + } + _ => {} // ignored + } + } +} + + +impl Dispatch for State { + fn event( + state: &mut Self, + seat: &WlSeat, + _: wl_seat::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + if state.seats.iter().find(|s| s == &seat).is_none() { + state.seats.push(seat.clone()); + } + } +} diff --git a/applets/cosmic-app-list/src/wayland_source.rs b/applets/cosmic-app-list/src/wayland_source.rs new file mode 100644 index 00000000..028c9e56 --- /dev/null +++ b/applets/cosmic-app-list/src/wayland_source.rs @@ -0,0 +1,219 @@ +//! Utilities for using an [`EventQueue`] from wayland-client with an event loop that performs polling with +//! [`calloop`](https://crates.io/crates/calloop). + +use std::{io, os::unix::prelude::RawFd}; + +use calloop::{ + generic::Generic, EventSource, InsertError, Interest, LoopHandle, Mode, Poll, PostAction, + Readiness, RegistrationToken, Token, TokenFactory, +}; +use nix::errno::Errno; +use wayland_backend::client::{ReadEventsGuard, WaylandError}; +use wayland_client::{DispatchError, EventQueue}; + +/// An adapter to insert an [`EventQueue`] into a calloop [`EventLoop`](calloop::EventLoop). +/// +/// This type implements [`EventSource`] which generates an event whenever events on the display need to be +/// dispatched. The event queue available in the callback calloop registers may be used to dispatch pending +/// events using [`EventQueue::dispatch_pending`]. +/// +/// [`WaylandSource::insert`] can be used to insert this source into an event loop and automatically dispatch +/// pending events on the display. +#[derive(Debug)] +pub struct WaylandSource { + queue: EventQueue, + fd: Generic, + read_guard: Option, +} + +impl WaylandSource { + /// Wrap an [`EventQueue`] as a [`WaylandSource`]. + pub fn new(queue: EventQueue) -> Result, WaylandError> { + let guard = queue.prepare_read()?; + let fd = Generic::new(guard.connection_fd(), Interest::READ, Mode::Level); + drop(guard); + + Ok(WaylandSource { + queue, + fd, + read_guard: None, + }) + } + + /// Access the underlying event queue + /// + /// Note that you should be careful when interacting with it if you invoke methods that + /// interact with the wayland socket (such as `dispatch()` or `prepare_read()`). These may + /// interfere with the proper waking up of this event source in the event loop. + pub fn queue(&mut self) -> &mut EventQueue { + &mut self.queue + } + + /// Insert this source into the given event loop. + /// + /// This adapter will pass the event loop's shared data as the `D` type for the event loop. + pub fn insert(self, handle: LoopHandle) -> Result> + where + D: 'static, + { + handle.insert_source(self, |_, queue, data| queue.dispatch_pending(data)) + } +} + +impl EventSource for WaylandSource { + type Event = (); + + /// The underlying event queue. + /// + /// You should call [`EventQueue::dispatch_pending`] inside your callback using this queue. + type Metadata = EventQueue; + type Ret = Result; + type Error = calloop::Error; + + fn process_events( + &mut self, + readiness: Readiness, + token: Token, + mut callback: F, + ) -> Result + where + F: FnMut(Self::Event, &mut Self::Metadata) -> Self::Ret, + { + let queue = &mut self.queue; + let read_guard = &mut self.read_guard; + + let action = self.fd.process_events(readiness, token, |_, _| { + // 1. read events from the socket if any are available + if let Some(guard) = read_guard.take() { + // might be None if some other thread read events before us, concurrently + if let Err(WaylandError::Io(err)) = guard.read() { + if err.kind() != io::ErrorKind::WouldBlock { + return Err(err); + } + } + } + + // 2. dispatch any pending events in the queue + // This is done to ensure we are not waiting for messages that are already in the buffer. + Self::loop_callback_pending(queue, &mut callback)?; + *read_guard = Some(Self::prepare_read(queue)?); + + // 3. Once dispatching is finished, flush the responses to the compositor + if let Err(WaylandError::Io(e)) = queue.flush() { + if e.kind() != io::ErrorKind::WouldBlock { + // in case of error, forward it and fast-exit + return Err(e); + } + // WouldBlock error means the compositor could not process all our messages + // quickly. Either it is slowed down or we are a spammer. + // Should not really happen, if it does we do nothing and will flush again later + } + + Ok(PostAction::Continue) + })?; + + Ok(action) + } + + fn register( + &mut self, + poll: &mut Poll, + token_factory: &mut TokenFactory, + ) -> calloop::Result<()> { + self.fd.register(poll, token_factory) + } + + fn reregister( + &mut self, + poll: &mut Poll, + token_factory: &mut TokenFactory, + ) -> calloop::Result<()> { + self.fd.reregister(poll, token_factory) + } + + fn unregister(&mut self, poll: &mut Poll) -> calloop::Result<()> { + self.fd.unregister(poll) + } + + fn pre_run(&mut self, mut callback: F) -> calloop::Result<()> + where + F: FnMut((), &mut Self::Metadata) -> Self::Ret, + { + debug_assert!(self.read_guard.is_none()); + + // flush the display before starting to poll + if let Err(WaylandError::Io(err)) = self.queue.flush() { + if err.kind() != io::ErrorKind::WouldBlock { + // in case of error, don't prepare a read, if the error is persistent, it'll trigger in other + // wayland methods anyway + log::error!("Error trying to flush the wayland display: {}", err); + return Err(err.into()); + } + } + + // ensure we are not waiting for messages that are already in the buffer. + Self::loop_callback_pending(&mut self.queue, &mut callback)?; + self.read_guard = Some(Self::prepare_read(&mut self.queue)?); + + Ok(()) + } + + fn post_run(&mut self, _: F) -> calloop::Result<()> + where + F: FnMut((), &mut Self::Metadata) -> Self::Ret, + { + // Drop implementation of ReadEventsGuard will do cleanup + self.read_guard.take(); + Ok(()) + } +} + +impl WaylandSource { + /// Loop over the callback until all pending messages have been dispatched. + fn loop_callback_pending(queue: &mut EventQueue, callback: &mut F) -> io::Result<()> + where + F: FnMut((), &mut EventQueue) -> Result, + { + // Loop on the callback until no pending events are left. + loop { + match callback((), queue) { + // No more pending events. + Ok(0) => break Ok(()), + + Ok(_) => continue, + + Err(DispatchError::Backend(WaylandError::Io(err))) => { + return Err(err); + } + + Err(DispatchError::Backend(WaylandError::Protocol(err))) => { + log::error!("Protocol error received on display: {}", err); + + break Err(Errno::EPROTO.into()); + } + + Err(DispatchError::BadMessage { msg, interface }) => { + log::error!( + "Bad message on interface \"{}\": (opcode: {}, args: {:?})", + interface, + msg.opcode, + msg.args, + ); + + break Err(Errno::EPROTO.into()); + } + } + } + } + + fn prepare_read(queue: &mut EventQueue) -> io::Result { + queue.prepare_read().map_err(|err| match err { + WaylandError::Io(err) => err, + + WaylandError::Protocol(err) => { + log::error!("Protocol error received on display: {}", err); + Errno::EPROTO.into() + } + }) + } +} diff --git a/applets/cosmic-applet-graphics/src/main.rs b/applets/cosmic-applet-graphics/src/main.rs index 97f4327b..f29ce638 100644 --- a/applets/cosmic-applet-graphics/src/main.rs +++ b/applets/cosmic-applet-graphics/src/main.rs @@ -51,8 +51,6 @@ fn main() { gtk4::STYLE_PROVIDER_PRIORITY_APPLICATION, ); - - let current_graphics = RT .block_on(get_current_graphics()) .expect("failed to connect to system76-power"); diff --git a/applets/cosmic-applet-workspaces/Cargo.toml b/applets/cosmic-applet-workspaces/Cargo.toml index f45b6c3d..d4d00bb7 100644 --- a/applets/cosmic-applet-workspaces/Cargo.toml +++ b/applets/cosmic-applet-workspaces/Cargo.toml @@ -18,9 +18,8 @@ i18n-embed = { version = "0.13.4", features = ["fluent-system", "desktop-request i18n-embed-fl = "0.6.4" rust-embed = "6.3.0" tokio = { version = "1.16.1", features = ["sync"] } -wayland-commons = "0.29.4" -wayland-backend = { version = "0.1.0-beta.7" } -wayland-client = { version = "0.30.0-beta.7" } +wayland-backend = { version = "0.1.0-beta.8" } +wayland-client = { version = "0.30.0-beta.8" } calloop = "*" nix = "*" log = "0.4" diff --git a/applets/cosmic-applet-workspaces/data/resources/ext-workspace-unstable-v1.xml b/applets/cosmic-applet-workspaces/data/resources/ext-workspace-unstable-v1.xml deleted file mode 100644 index 24410b62..00000000 --- a/applets/cosmic-applet-workspaces/data/resources/ext-workspace-unstable-v1.xml +++ /dev/null @@ -1,306 +0,0 @@ - - - - Copyright © 2019 Christopher Billington - Copyright © 2020 Ilia Bozhinov - - Permission to use, copy, modify, distribute, and sell this - software and its documentation for any purpose is hereby granted - without fee, provided that the above copyright notice appear in - all copies and that both that copyright notice and this permission - notice appear in supporting documentation, and that the name of - the copyright holders not be used in advertising or publicity - pertaining to distribution of the software without specific, - written prior permission. The copyright holders make no - representations about the suitability of this software for any - purpose. It is provided "as is" without express or implied - warranty. - - THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS - SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND - FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY - SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN - AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, - ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF - THIS SOFTWARE. - - - - - Workspaces, also called virtual desktops, are groups of surfaces. A - compositor with a concept of workspaces may only show some such groups of - surfaces (those of 'active' workspaces) at a time. 'Activating' a - workspace is a request for the compositor to display that workspace's - surfaces as normal, whereas the compositor may hide or otherwise - de-emphasise surfaces that are associated only with 'inactive' workspaces. - Workspaces are grouped by which sets of outputs they correspond to, and - may contain surfaces only from those outputs. In this way, it is possible - for each output to have its own set of workspaces, or for all outputs (or - any other arbitrary grouping) to share workspaces. Compositors may - optionally conceptually arrange each group of workspaces in an - N-dimensional grid. - - The purpose of this protocol is to enable the creation of taskbars and - docks by providing them with a list of workspaces and their properties, - and allowing them to activate and deactivate workspaces. - - After a client binds the zext_workspace_manager_v1, each workspace will be - sent via the workspace event. - - - - - This event is emitted whenever a new workspace group has been created. - - All initial details of the workspace group (workspaces, outputs) will be - sent immediately after this event via the corresponding events in - zext_workspace_group_handle_v1. - - - - - - - The client must send this request after it has finished sending other - requests. The compositor must process a series of requests preceding a - commit request atomically. - - This allows changes to the workspace properties to be seen as atomic, - even if they happen via multiple events, and even if they involve - multiple zext_workspace_handle_v1 objects, for example, deactivating one - workspace and activating another. - - - - - - This event is sent after all changes in all workspace groups have been - sent. - - This allows changes to one or more zext_workspace_group_handle_v1 - properties to be seen as atomic, even if they happen via multiple - events. In particular, an output moving from one workspace group to - another sends an output_enter event and an output_leave event to the two - zext_workspace_group_handle_v1 objects in question. The compositor sends - the done event only after updating the output information in both - workspace groups. - - - - - - This event indicates that the compositor is done sending events to the - zext_workspace_manager_v1. The server will destroy the object - immediately after sending this request, so it will become invalid and - the client should free any resources associated with it. - - - - - - Indicates the client no longer wishes to receive events for new - workspace groups. However the compositor may emit further workspace - events, until the finished event is emitted. - - The client must not send any more requests after this one. - - - - - - - A zext_workspace_group_handle_v1 object represents a a workspace group - that is assigned a set of outputs and contains a number of workspaces. - - The set of outputs assigned to the workspace group is conveyed to the client via - output_enter and output_leave events, and its workspaces are conveyed with - workspace events. - - For example, a compositor which has a set of workspaces for each output may - advertise a workspace group (and its workspaces) per output, whereas a compositor - where a workspace spans all outputs may advertise a single workspace group for all - outputs. - - - - - This event is emitted whenever an output is assigned to the workspace - group. - - - - - - - This event is emitted whenever an output is removed from the workspace - group. - - - - - - - This event is emitted whenever a new workspace has been created. - - All initial details of the workspace (name, coordinates, state) will - be sent immediately after this event via the corresponding events in - zext_workspace_handle_v1. - - - - - - - This event means the zext_workspace_group_handle_v1 has been destroyed. - It is guaranteed there won't be any more events for this - zext_workspace_group_handle_v1. The zext_workspace_group_handle_v1 becomes - inert so any requests will be ignored except the destroy request. - - The compositor must remove all workspaces belonging to a workspace group - before removing the workspace group. - - - - - - Request that the compositor create a new workspace with the given name. - - There is no guarantee that the compositor will create a new workspace, - or that the created workspace will have the provided name. - - - - - - - Destroys the zext_workspace_handle_v1 object. - - This request should be called either when the client does not want to - use the workspace object any more or after the remove event to finalize - the destruction of the object. - - - - - - - A zext_workspace_handle_v1 object represents a a workspace that handles a - group of surfaces. - - Each workspace has a name, conveyed to the client with the name event; a - list of states, conveyed to the client with the state event; and - optionally a set of coordinates, conveyed to the client with the - coordinates event. The client may request that the compositor activate or - deactivate the workspace. - - Each workspace can belong to only a single workspace group. - Depepending on the compositor policy, there might be workspaces with - the same name in different workspace groups, but these workspaces are still - separate (e.g. one of them might be active while the other is not). - - - - - This event is emitted immediately after the zext_workspace_handle_v1 is - created and whenever the name of the workspace changes. - - - - - - - This event is used to organize workspaces into an N-dimensional grid - within a workspace group, and if supported, is emitted immediately after - the zext_workspace_handle_v1 is created and whenever the coordinates of - the workspace change. Compositors may not send this event if they do not - conceptually arrange workspaces in this way. If compositors simply - number workspaces, without any geometric interpretation, they may send - 1D coordinates, which clients should not interpret as implying any - geometry. Sending an empty array means that the compositor no longer - orders the workspace geometrically. - - Coordinates have an arbitrary number of dimensions N with an uint32 - position along each dimension. By convention if N > 1, the first - dimension is X, the second Y, the third Z, and so on. The compositor may - chose to utilize these events for a more novel workspace layout - convention, however. No guarantee is made about the grid being filled or - bounded; there may be a workspace at coordinate 1 and another at - coordinate 1000 and none in between. Within a workspace group, however, - workspaces must have unique coordinates of equal dimensionality. - - - - - - - This event is emitted immediately after the zext_workspace_handle_v1 is - created and each time the workspace state changes, either because of a - compositor action or because of a request in this protocol. - - - - - - - The different states that a workspace can have. - - - - - - - The workspace is not visible in its workspace group, and clients - attempting to visualize the compositor workspace state should not - display such workspaces. - - - - - - - This event means the zext_workspace_handle_v1 has been destroyed. It is - guaranteed there won't be any more events for this - zext_workspace_handle_v1. The zext_workspace_handle_v1 becomes inert so - any requests will be ignored except the destroy request. - - - - - - Destroys the zext_workspace_handle_v1 object. - - This request should be called either when the client does not want to - use the workspace object any more or after the remove event to finalize - the destruction of the object. - - - - - - Request that this workspace be activated. - - There is no guarantee the workspace will be actually activated, and - behaviour may be compositor-dependent. For example, activating a - workspace may or may not deactivate all other workspaces in the same - group. - - - - - - Request that this workspace be deactivated. - - There is no guarantee the workspace will be actually deactivated. - - - - - - Request that this workspace be removed. - - There is no guarantee the workspace will be actually removed. - - - - diff --git a/applets/cosmic-applet-workspaces/src/wayland.rs b/applets/cosmic-applet-workspaces/src/wayland.rs index 2e7d7401..3d696ac0 100644 --- a/applets/cosmic-applet-workspaces/src/wayland.rs +++ b/applets/cosmic-applet-workspaces/src/wayland.rs @@ -2,7 +2,13 @@ use crate::{ utils::{Activate, WorkspaceEvent}, wayland_source::WaylandSource, }; +use calloop::channel::*; use cosmic_panel_config::CosmicPanelConfig; +use cosmic_protocols::workspace::v1::client::{ + zcosmic_workspace_group_handle_v1::{self, ZcosmicWorkspaceGroupHandleV1}, + zcosmic_workspace_handle_v1::{self, ZcosmicWorkspaceHandleV1}, + zcosmic_workspace_manager_v1::{self, ZcosmicWorkspaceManagerV1}, +}; use gtk4::glib; use std::{ collections::HashMap, env, hash::Hash, mem, os::unix::net::UnixStream, path::PathBuf, @@ -18,18 +24,12 @@ use wayland_client::{ }, ConnectError, Proxy, }; -use cosmic_protocols::workspace::v1::client::{ - zcosmic_workspace_manager_v1::{self, ZcosmicWorkspaceManagerV1}, - zcosmic_workspace_group_handle_v1::{self, ZcosmicWorkspaceGroupHandleV1}, - zcosmic_workspace_handle_v1::{self, ZcosmicWorkspaceHandleV1}, -}; use wayland_client::{Connection, Dispatch, QueueHandle}; -use calloop::channel::*; pub fn spawn_workspaces(tx: glib::Sender) -> SyncSender { - let (workspaces_tx, mut workspaces_rx) = calloop::channel::sync_channel(100); + let (workspaces_tx, workspaces_rx) = calloop::channel::sync_channel(100); - if let Ok(Ok(conn)) = std::env::var("HOST_WAYLAND_DISPLAY") + if let Ok(Ok(conn)) = std::env::var("WAYLAND_DISPLAY") .map_err(anyhow::Error::msg) .map(|display_str| { let mut socket_path = env::var_os("XDG_RUNTIME_DIR") @@ -80,19 +80,18 @@ pub fn spawn_workspaces(tx: glib::Sender) -> SyncSender { } } Event::Msg(WorkspaceEvent::Scroll(v)) => { - if let Some((w_g, w_i)) = state - .workspace_groups - .iter() - .find_map(|g| { - if g.output != state.expected_output { - return None; - } - g.workspaces - .iter() - .position(|w| w.states.contains(&zcosmic_workspace_handle_v1::State::Active)) - .map(|w_i| (g, w_i)) - }) - { + if let Some((w_g, w_i)) = state.workspace_groups.iter().find_map(|g| { + if g.output != state.expected_output { + return None; + } + g.workspaces + .iter() + .position(|w| { + w.states + .contains(&zcosmic_workspace_handle_v1::State::Active) + }) + .map(|w_i| (g, w_i)) + }) { let max_w = w_g.workspaces.len().wrapping_sub(1); let d_i = if v > 0.0 { if w_i == max_w { @@ -130,7 +129,7 @@ pub fn spawn_workspaces(tx: glib::Sender) -> SyncSender { } }); } else { - eprintln!("ENV variable HOST_WAYLAND_DISPLAY is missing. Exiting..."); + eprintln!("ENV variable WAYLAND_DISPLAY is missing. Exiting..."); std::process::exit(1); } @@ -154,12 +153,17 @@ impl State { .iter() .filter_map(|g| { if g.output == self.expected_output { - Some(g.workspaces.iter().map(|w| (w.name.clone(), match &w.states { - x if x.contains(&zcosmic_workspace_handle_v1::State::Active) => 0, - x if x.contains(&zcosmic_workspace_handle_v1::State::Urgent) => 1, - x if x.contains(&zcosmic_workspace_handle_v1::State::Hidden) => 2, - _ => 3, - }))) + Some(g.workspaces.iter().map(|w| { + ( + w.name.clone(), + match &w.states { + x if x.contains(&zcosmic_workspace_handle_v1::State::Active) => 0, + x if x.contains(&zcosmic_workspace_handle_v1::State::Urgent) => 1, + x if x.contains(&zcosmic_workspace_handle_v1::State::Hidden) => 2, + _ => 3, + }, + ) + })) } else { None } @@ -185,7 +189,7 @@ struct Workspace { impl Dispatch for State { fn event( - &mut self, + state: &mut Self, registry: &wl_registry::WlRegistry, event: wl_registry::Event, _: &(), @@ -201,14 +205,9 @@ impl Dispatch for State { match &interface[..] { "zcosmic_workspace_manager_v1" => { let workspace_manager = registry - .bind::( - name, - 1, - qh, - (), - ) + .bind::(name, 1, qh, ()) .unwrap(); - self.workspace_manager = Some(workspace_manager); + state.workspace_manager = Some(workspace_manager); } "wl_output" => { registry.bind::(name, 1, qh, ()).unwrap(); @@ -221,7 +220,7 @@ impl Dispatch for State { impl Dispatch for State { fn event( - &mut self, + state: &mut Self, _: &ZcosmicWorkspaceManagerV1, event: zcosmic_workspace_manager_v1::Event, _: &(), @@ -230,26 +229,28 @@ impl Dispatch for State { ) { match event { zcosmic_workspace_manager_v1::Event::WorkspaceGroup { workspace_group } => { - self.workspace_groups.push(WorkspaceGroup { + state.workspace_groups.push(WorkspaceGroup { workspace_group_handle: workspace_group, output: None, workspaces: Vec::new(), }); } zcosmic_workspace_manager_v1::Event::Done => { - for group in &mut self.workspace_groups { + for group in &mut state.workspace_groups { group.workspaces.sort_by(|w1, w2| { - w1.coordinates.iter().zip(w2.coordinates.iter()) + w1.coordinates + .iter() + .zip(w2.coordinates.iter()) .skip_while(|(coord1, coord2)| coord1 == coord2) .next() .map(|(coord1, coord2)| coord1.cmp(coord2)) .unwrap_or(std::cmp::Ordering::Equal) }); } - let _ = self.tx.send(self.clone()); + let _ = state.tx.send(state.clone()); } zcosmic_workspace_manager_v1::Event::Finished => { - self.workspace_manager.take(); + state.workspace_manager.take(); } _ => {} } @@ -262,7 +263,7 @@ impl Dispatch for State { impl Dispatch for State { fn event( - &mut self, + state: &mut Self, group: &ZcosmicWorkspaceGroupHandleV1, event: zcosmic_workspace_group_handle_v1::Event, _: &(), @@ -271,7 +272,7 @@ impl Dispatch for State { ) { match event { zcosmic_workspace_group_handle_v1::Event::OutputEnter { output } => { - if let Some(group) = self + if let Some(group) = state .workspace_groups .iter_mut() .find(|g| &g.workspace_group_handle == group) @@ -280,14 +281,14 @@ impl Dispatch for State { } } zcosmic_workspace_group_handle_v1::Event::OutputLeave { output } => { - if let Some(group) = self.workspace_groups.iter_mut().find(|g| { + if let Some(group) = state.workspace_groups.iter_mut().find(|g| { &g.workspace_group_handle == group && g.output.as_ref() == Some(&output) }) { group.output = None; } } zcosmic_workspace_group_handle_v1::Event::Workspace { workspace } => { - if let Some(group) = self + if let Some(group) = state .workspace_groups .iter_mut() .find(|g| &g.workspace_group_handle == group) @@ -301,12 +302,12 @@ impl Dispatch for State { } } zcosmic_workspace_group_handle_v1::Event::Remove => { - if let Some(group) = self + if let Some(group) = state .workspace_groups .iter() .position(|g| &g.workspace_group_handle == group) { - self.workspace_groups.remove(group); + state.workspace_groups.remove(group); } } _ => {} @@ -320,7 +321,7 @@ impl Dispatch for State { impl Dispatch for State { fn event( - &mut self, + state: &mut Self, workspace: &ZcosmicWorkspaceHandleV1, event: zcosmic_workspace_handle_v1::Event, _: &(), @@ -329,7 +330,7 @@ impl Dispatch for State { ) { match event { zcosmic_workspace_handle_v1::Event::Name { name } => { - if let Some(w) = self.workspace_groups.iter_mut().find_map(|g| { + if let Some(w) = state.workspace_groups.iter_mut().find_map(|g| { g.workspaces .iter_mut() .find(|w| &w.workspace_handle == workspace) @@ -338,27 +339,40 @@ impl Dispatch for State { } } zcosmic_workspace_handle_v1::Event::Coordinates { coordinates } => { - if let Some(w) = self.workspace_groups.iter_mut().find_map(|g| { + if let Some(w) = state.workspace_groups.iter_mut().find_map(|g| { g.workspaces .iter_mut() .find(|w| &w.workspace_handle == workspace) }) { // wayland is host byte order - w.coordinates = coordinates.chunks(4).map(|chunk| u32::from_ne_bytes(chunk.try_into().unwrap())).collect(); + w.coordinates = coordinates + .chunks(4) + .map(|chunk| u32::from_ne_bytes(chunk.try_into().unwrap())) + .collect(); } } - zcosmic_workspace_handle_v1::Event::State { state } => { - if let Some(w) = self.workspace_groups.iter_mut().find_map(|g| { + zcosmic_workspace_handle_v1::Event::State { + state: workspace_state, + } => { + if let Some(w) = state.workspace_groups.iter_mut().find_map(|g| { g.workspaces .iter_mut() .find(|w| &w.workspace_handle == workspace) }) { // wayland is host byte order - w.states = state.chunks(4).map(|chunk| zcosmic_workspace_handle_v1::State::try_from(u32::from_ne_bytes(chunk.try_into().unwrap())).unwrap()).collect(); + w.states = workspace_state + .chunks(4) + .map(|chunk| { + zcosmic_workspace_handle_v1::State::try_from(u32::from_ne_bytes( + chunk.try_into().unwrap(), + )) + .unwrap() + }) + .collect(); } } zcosmic_workspace_handle_v1::Event::Remove => { - if let Some((g, w_i)) = self.workspace_groups.iter_mut().find_map(|g| { + if let Some((g, w_i)) = state.workspace_groups.iter_mut().find_map(|g| { g.workspaces .iter_mut() .position(|w| &w.workspace_handle == workspace) @@ -374,7 +388,7 @@ impl Dispatch for State { impl Dispatch for State { fn event( - &mut self, + state: &mut Self, o: &WlOutput, e: wl_output::Event, _: &(), @@ -382,8 +396,8 @@ impl Dispatch for State { _: &QueueHandle, ) { match e { - wl_output::Event::Name { name } if name == self.configured_output => { - self.expected_output.replace(o.clone()); + wl_output::Event::Name { name } if name == state.configured_output => { + state.expected_output.replace(o.clone()); } _ => {} // ignored }