diff --git a/Cargo.lock b/Cargo.lock index 2e97bd7b..a6a8f2cb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -312,7 +312,7 @@ dependencies = [ "cosmic-panel-config", "futures", "futures-util", - "gio", + "gio 0.16.0", "glib-build-tools", "gsk4", "gtk4", @@ -458,19 +458,23 @@ name = "cosmic-applet-workspaces" version = "0.1.0" dependencies = [ "anyhow", + "calloop", "cascade", "cosmic-panel-config", - "glib-build-tools", + "gio 0.15.12", "gtk4", "i18n-embed", "i18n-embed-fl", + "log", + "nix 0.24.1", "once_cell", "pretty_env_logger", "rust-embed", "tokio", - "wayland-client", + "wayland-backend", + "wayland-client 0.30.0-beta.7", "wayland-commons", - "wayland-scanner", + "wayland-scanner 0.30.0-beta.7", ] [[package]] @@ -981,7 +985,7 @@ source = "git+https://github.com/gtk-rs/gtk-rs-core#57cc8f1d7ead0e0c1569d04485b4 dependencies = [ "bitflags", "gdk-pixbuf-sys", - "gio", + "gio 0.16.0", "glib 0.16.0", "libc", ] @@ -991,7 +995,7 @@ name = "gdk-pixbuf-sys" version = "0.16.0" source = "git+https://github.com/gtk-rs/gtk-rs-core#57cc8f1d7ead0e0c1569d04485b474450b02eb55" dependencies = [ - "gio-sys", + "gio-sys 0.16.0", "glib-sys 0.16.0", "gobject-sys 0.16.0", "libc", @@ -1007,7 +1011,7 @@ dependencies = [ "cairo-rs", "gdk-pixbuf", "gdk4-sys", - "gio", + "gio 0.16.0", "glib 0.16.0", "libc", "pango", @@ -1020,7 +1024,7 @@ source = "git+https://github.com/gtk-rs/gtk4-rs#92170a7a685c0336e53da58d85779635 dependencies = [ "cairo-sys-rs", "gdk-pixbuf-sys", - "gio-sys", + "gio-sys 0.16.0", "glib-sys 0.16.0", "gobject-sys 0.16.0", "libc", @@ -1036,7 +1040,7 @@ source = "git+https://github.com/gtk-rs/gtk4-rs#92170a7a685c0336e53da58d85779635 dependencies = [ "gdk4", "gdk4-x11-sys", - "gio", + "gio 0.16.0", "glib 0.16.0", "libc", "x11", @@ -1096,6 +1100,23 @@ dependencies = [ "temp-dir", ] +[[package]] +name = "gio" +version = "0.15.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68fdbc90312d462781a395f7a16d96a2b379bb6ef8cd6310a2df272771c4283b" +dependencies = [ + "bitflags", + "futures-channel", + "futures-core", + "futures-io", + "gio-sys 0.15.10", + "glib 0.15.12", + "libc", + "once_cell", + "thiserror", +] + [[package]] name = "gio" version = "0.16.0" @@ -1106,13 +1127,26 @@ dependencies = [ "futures-core", "futures-io", "futures-util", - "gio-sys", + "gio-sys 0.16.0", "glib 0.16.0", "libc", "once_cell", "thiserror", ] +[[package]] +name = "gio-sys" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32157a475271e2c4a023382e9cab31c4584ee30a97da41d3c4e9fdd605abcf8d" +dependencies = [ + "glib-sys 0.15.10", + "gobject-sys 0.15.10", + "libc", + "system-deps", + "winapi", +] + [[package]] name = "gio-sys" version = "0.16.0" @@ -1312,7 +1346,7 @@ dependencies = [ "futures-channel", "gdk-pixbuf", "gdk4", - "gio", + "gio 0.16.0", "glib 0.16.0", "graphene-rs", "gsk4", @@ -1345,7 +1379,7 @@ dependencies = [ "cairo-sys-rs", "gdk-pixbuf-sys", "gdk4-sys", - "gio-sys", + "gio-sys 0.16.0", "glib-sys 0.16.0", "gobject-sys 0.16.0", "graphene-sys", @@ -1555,7 +1589,7 @@ dependencies = [ "cascade", "gdk4", "gdk4-x11", - "gio", + "gio 0.16.0", "gobject-sys 0.16.0", "gtk4", "libcosmic-widgets", @@ -2521,7 +2555,7 @@ dependencies = [ "wayland-commons", "wayland-protocols", "wayland-server", - "wayland-sys", + "wayland-sys 0.29.4", "xkbcommon", ] @@ -2538,7 +2572,7 @@ dependencies = [ "memmap2", "nix 0.22.3", "pkg-config", - "wayland-client", + "wayland-client 0.29.4", "wayland-cursor", "wayland-protocols", ] @@ -2890,6 +2924,20 @@ version = "0.2.80" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d554b7f530dee5964d9a9468d95c1f8b8acae4f282807e7d27d4b03099a46744" +[[package]] +name = "wayland-backend" +version = "0.1.0-beta.7" +source = "git+https://github.com/smithay/wayland-rs.git#7f76abda948a5065951518e1af7c5046725dd6d1" +dependencies = [ + "cc", + "downcast-rs", + "log", + "nix 0.24.1", + "scoped-tls", + "smallvec", + "wayland-sys 0.30.0-beta.7", +] + [[package]] name = "wayland-client" version = "0.29.4" @@ -2902,8 +2950,23 @@ dependencies = [ "nix 0.22.3", "scoped-tls", "wayland-commons", - "wayland-scanner", - "wayland-sys", + "wayland-scanner 0.29.4", + "wayland-sys 0.29.4", +] + +[[package]] +name = "wayland-client" +version = "0.30.0-beta.7" +source = "git+https://github.com/smithay/wayland-rs.git#7f76abda948a5065951518e1af7c5046725dd6d1" +dependencies = [ + "bitflags", + "futures-channel", + "futures-core", + "log", + "nix 0.24.1", + "thiserror", + "wayland-backend", + "wayland-scanner 0.30.0-beta.7", ] [[package]] @@ -2915,7 +2978,7 @@ dependencies = [ "nix 0.22.3", "once_cell", "smallvec", - "wayland-sys", + "wayland-sys 0.29.4", ] [[package]] @@ -2925,7 +2988,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c52758f13d5e7861fc83d942d3d99bf270c83269575e52ac29e5b73cb956a6bd" dependencies = [ "nix 0.22.3", - "wayland-client", + "wayland-client 0.29.4", "xcursor", ] @@ -2935,8 +2998,8 @@ version = "0.29.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83281d69ee162b59031c666385e93bde4039ec553b90c4191cdb128ceea29a3a" dependencies = [ - "wayland-client", - "wayland-sys", + "wayland-client 0.29.4", + "wayland-sys 0.29.4", ] [[package]] @@ -2946,9 +3009,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60147ae23303402e41fe034f74fb2c35ad0780ee88a1c40ac09a3be1e7465741" dependencies = [ "bitflags", - "wayland-client", + "wayland-client 0.29.4", "wayland-commons", - "wayland-scanner", + "wayland-scanner 0.29.4", "wayland-server", ] @@ -2963,6 +3026,17 @@ dependencies = [ "xml-rs", ] +[[package]] +name = "wayland-scanner" +version = "0.30.0-beta.7" +source = "git+https://github.com/smithay/wayland-rs.git#7f76abda948a5065951518e1af7c5046725dd6d1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "xml-rs", +] + [[package]] name = "wayland-server" version = "0.29.4" @@ -2977,8 +3051,8 @@ dependencies = [ "parking_lot 0.11.2", "scoped-tls", "wayland-commons", - "wayland-scanner", - "wayland-sys", + "wayland-scanner 0.29.4", + "wayland-sys 0.29.4", ] [[package]] @@ -2993,6 +3067,16 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "wayland-sys" +version = "0.30.0-beta.7" +source = "git+https://github.com/smithay/wayland-rs.git#7f76abda948a5065951518e1af7c5046725dd6d1" +dependencies = [ + "dlib", + "log", + "pkg-config", +] + [[package]] name = "wepoll-ffi" version = "0.1.2" diff --git a/applets/cosmic-app-list/src/apps_window/mod.rs b/applets/cosmic-app-list/src/apps_window/mod.rs index 852c97ed..2f454905 100644 --- a/applets/cosmic-app-list/src/apps_window/mod.rs +++ b/applets/cosmic-app-list/src/apps_window/mod.rs @@ -21,8 +21,8 @@ glib::wrapper! { impl CosmicAppListWindow { pub fn new(app: >k4::Application, tx: mpsc::Sender) -> Self { - let self_: Self = Object::new(&[("application", app)]) - .expect("Failed to create `CosmicAppListWindow`."); + let self_: Self = + Object::new(&[("application", app)]).expect("Failed to create `CosmicAppListWindow`."); let imp = imp::CosmicAppListWindow::from_instance(&self_); cascade! { diff --git a/applets/cosmic-app-list/src/dock_item/mod.rs b/applets/cosmic-app-list/src/dock_item/mod.rs index 3dd7ed67..bc776407 100644 --- a/applets/cosmic-app-list/src/dock_item/mod.rs +++ b/applets/cosmic-app-list/src/dock_item/mod.rs @@ -174,7 +174,6 @@ impl DockItem { PanelAnchor::Top => PositionType::Bottom, PanelAnchor::Bottom => PositionType::Top, }); - } pub fn add_popover(&self, obj: &DockObject) { diff --git a/applets/cosmic-app-list/src/dock_object/mod.rs b/applets/cosmic-app-list/src/dock_object/mod.rs index b7a7d26a..f484477d 100644 --- a/applets/cosmic-app-list/src/dock_object/mod.rs +++ b/applets/cosmic-app-list/src/dock_object/mod.rs @@ -49,16 +49,19 @@ impl DockObject { pub fn get_name(&self) -> Option { let imp = imp::DockObject::from_instance(self); - imp.appinfo.borrow().as_ref().map(|app_info| app_info.name().to_string()) + imp.appinfo + .borrow() + .as_ref() + .map(|app_info| app_info.name().to_string()) } pub fn get_image(&self) -> gtk4::Image { let imp = imp::DockObject::from_instance(self); if let Some(app_info) = imp.appinfo.borrow().as_ref() { let image = Image::new(); - let icon = app_info - .icon() - .unwrap_or_else(|| Icon::for_string("image-missing").expect("Failed to set default icon")); + let icon = app_info.icon().unwrap_or_else(|| { + Icon::for_string("image-missing").expect("Failed to set default icon") + }); image.set_from_gicon(&icon); image.set_tooltip_text(None); image diff --git a/applets/cosmic-app-list/src/main.rs b/applets/cosmic-app-list/src/main.rs index 3a0798a6..3da8b078 100644 --- a/applets/cosmic-app-list/src/main.rs +++ b/applets/cosmic-app-list/src/main.rs @@ -82,8 +82,6 @@ fn main() { // .expect("Failed to close selected window"); } Event::Favorite((name, should_favorite)) => { - dbg!(&name); - dbg!(should_favorite); let saved_app_model = apps_container.model(DockListType::Saved); let active_app_model = apps_container.model(DockListType::Active); if should_favorite { @@ -128,7 +126,7 @@ fn main() { let cached_results = cached_results.as_ref().lock().unwrap(); let stack_active = cached_results.iter().fold( BTreeMap::new(), - |mut acc: BTreeMap, elem:&Item| { + |mut acc: BTreeMap, elem: &Item| { if let Some(v) = acc.get_mut(&elem.description) { v.0.push(elem.clone()); } else { @@ -164,11 +162,7 @@ fn main() { // ); let active = stack_active.remove(i); dock_obj.set_property("active", active.to_value()); - saved_app_model.items_changed( - saved_i, - 0, - 0, - ); + saved_app_model.items_changed(saved_i, 0, 0); } else if cached_results .iter() .any(|s| s.description == cur_app_info.name()) @@ -177,11 +171,7 @@ fn main() { "active", BoxedWindowList(Vec::new()).to_value(), ); - saved_app_model.items_changed( - saved_i, - 0, - 0, - ); + saved_app_model.items_changed(saved_i, 0, 0); } } } @@ -236,11 +226,7 @@ fn main() { // 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, - ); + saved_app_model.items_changed(saved_i, 0, 0); } else if results .iter() .any(|s| s.description == cur_app_info.name()) @@ -249,11 +235,7 @@ fn main() { "active", BoxedWindowList(Vec::new()).to_value(), ); - saved_app_model.items_changed( - saved_i, - 0, - 0, - ); + saved_app_model.items_changed(saved_i, 0, 0); } } } diff --git a/applets/cosmic-applet-workspaces/Cargo.toml b/applets/cosmic-applet-workspaces/Cargo.toml index f52da322..4069b9f8 100644 --- a/applets/cosmic-applet-workspaces/Cargo.toml +++ b/applets/cosmic-applet-workspaces/Cargo.toml @@ -17,9 +17,13 @@ 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-client = "0.29.4" wayland-commons = "0.29.4" +wayland-scanner = { git = "https://github.com/smithay/wayland-rs.git", version = "0.30.0-beta.4"} +wayland-backend = { version = "0.1.0-beta.4", git = "https://github.com/smithay/wayland-rs.git" } +wayland-client = { version = "0.30.0-beta.4", git = "https://github.com/smithay/wayland-rs.git" } +calloop = "*" +nix = "*" +log = "0.4" [build-dependencies] -glib-build-tools = { git = "https://github.com/gtk-rs/gtk-rs-core" } -wayland-scanner = "0.29" +gio = "0.15.10" diff --git a/applets/cosmic-applet-workspaces/build.rs b/applets/cosmic-applet-workspaces/build.rs index 3d8c16e0..8f6b0f74 100644 --- a/applets/cosmic-applet-workspaces/build.rs +++ b/applets/cosmic-applet-workspaces/build.rs @@ -1,30 +1,26 @@ -extern crate wayland_scanner; - use std::{env, path::PathBuf, process::Command}; -use wayland_scanner::{generate_code, Side}; +// use wayland_scanner::{generate_client_code}; fn main() { if let Some(output) = Command::new("git") - .args(&["rev-parse", "HEAD"]) - .output() - .ok() -{ - let git_hash = String::from_utf8(output.stdout).unwrap(); - println!("cargo:rustc-env=GIT_HASH={}", git_hash); -} - glib_build_tools::compile_resources( + .args(&["rev-parse", "HEAD"]) + .output() + .ok() + { + let git_hash = String::from_utf8(output.stdout).unwrap(); + println!("cargo:rustc-env=GIT_HASH={}", git_hash); + } + gio::compile_resources( "data/resources", "data/resources/resources.gresource.xml", "compiled.gresource", ); - let dest = PathBuf::from(&env::var("OUT_DIR").unwrap()); - // Location of the xml file, relative to the `Cargo.toml` - let drm_protocol_file = "data/resources/wayland-drm.xml"; - let ext_workspace_protocol_file = "data/resources/ext-workspace-unstable-v1.xml"; - // Target directory for the generate files - generate_code( - ext_workspace_protocol_file, - &dest.join("ext_workspace.rs"), - Side::Client, - ); + // let dest = PathBuf::from(&env::var("OUT_DIR").unwrap()); + // // Location of the xml file, relative to the `Cargo.toml` + // let ext_workspace_protocol_file = "data/resources/ext-workspace-unstable-v1.xml"; + // // Target directory for the generate files + // generate_client_code!( + // ext_workspace_protocol_file, + // &dest.join("ext_workspace.rs"), + // ); } diff --git a/applets/cosmic-applet-workspaces/data/resources/style.css b/applets/cosmic-applet-workspaces/data/resources/style.css index e69de29b..8d1c87ed 100644 --- a/applets/cosmic-applet-workspaces/data/resources/style.css +++ b/applets/cosmic-applet-workspaces/data/resources/style.css @@ -0,0 +1,106 @@ +@define-color accent_color #001d6b; +@define-color destructive_color #aa3000; +@define-color view_bg_color #00000044; +@define-color destructive_fg_color white; +@define-color accent_fg_color white; +@define-color view_fg_color white; + +button.alert { + border-radius: 0; + padding: 0px; + background-color: @destructive_color; + background-image: none; + color: @destructive_fg_color; + border-color: transparent; + outline-color: transparent; +} + +button.active { + border-radius: 0; + padding: 0px; + background-color: @accent_color; + background-image: none; + color: @accent_fg_color; + border-color: transparent; + outline-color: transparent; +} + +button.inactive { + border-radius: 0; + padding: 0px; + background-color: @view_bg_color; + background-image: none; + color: @view_fg_color; + border-color: transparent; + outline-color: transparent; +} + +button.alert:hover { + border-radius: 0; + padding: 0px; + background-color: darken(@destructive_color); + background-image: none; + color: @destructive_fg_color; + border-color: transparent; + outline-color: transparent; +} + +button.active:hover { + border-radius: 0; + padding: 0px; + background-color: darken(@accent_color); + background-image: none; + color: @accent_fg_color; + border-color: transparent; + outline-color: transparent; +} + +button.inactive:hover { + border-radius: 0; + padding: 0px; + background-color: darken(@view_bg_color); + background-image: none; + color: @view_bg_color; + border-color: transparent; + outline-color: transparent; +} + +window { + background: transparent; +} + +listview { + border-color: transparent; + background: transparent; + outline-color: transparent; +} + +listview row { + padding-left: 0px; + padding-right: 0px; + padding-top: 0px; + padding-bottom: 0px; + background: transparent; + border-color: transparent; + outline-color: transparent; +} + +listview row:hover { + padding-left: 0px; + padding-right: 0px; + padding-top: 0px; + padding-bottom: 0px; + background: transparent; + border-color: transparent; + outline-color: transparent; +} + +label { + padding: 0px; + background-color: transparent; +} + +box { + padding: 0px; + background-color: transparent; +} \ No newline at end of file diff --git a/applets/cosmic-applet-workspaces/i18n/en/cosmic_applet_workspaces.ftl b/applets/cosmic-applet-workspaces/i18n/en/cosmic_applet_workspaces.ftl index 2d5ea1e4..86b3f7ca 100644 --- a/applets/cosmic-applet-workspaces/i18n/en/cosmic_applet_workspaces.ftl +++ b/applets/cosmic-applet-workspaces/i18n/en/cosmic_applet_workspaces.ftl @@ -1 +1 @@ -cosmic-app-list = Cosmic Dock App List +cosmic-applet-workspaces = Cosmic Workspaces diff --git a/applets/cosmic-applet-workspaces/src/ext-workspace-unstable-v1.xml b/applets/cosmic-applet-workspaces/src/ext-workspace-unstable-v1.xml new file mode 100644 index 00000000..24410b62 --- /dev/null +++ b/applets/cosmic-applet-workspaces/src/ext-workspace-unstable-v1.xml @@ -0,0 +1,306 @@ + + + + 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/main.rs b/applets/cosmic-applet-workspaces/src/main.rs index 21eb8dec..681a452b 100644 --- a/applets/cosmic-applet-workspaces/src/main.rs +++ b/applets/cosmic-applet-workspaces/src/main.rs @@ -3,26 +3,29 @@ use gtk4::{ gdk::Display, gio::{self, ApplicationFlags}, - glib, + glib::{self, MainContext, Priority}, prelude::*, CssProvider, StyleContext, }; use once_cell::sync::OnceCell; use std::sync::{Arc, Mutex}; use tokio::sync::mpsc; -use utils::{Activate, Workspace}; +use utils::{Activate, WorkspaceEvent}; +use wayland::State; use window::CosmicWorkspacesWindow; +use calloop::channel::SyncSender; mod localize; mod utils; mod wayland; +mod wayland_source; mod window; mod workspace_button; mod workspace_list; mod workspace_object; const ID: &str = "com.system76.CosmicAppletWorkspaces"; -static TX: OnceCell> = OnceCell::new(); +static TX: OnceCell> = OnceCell::new(); pub fn localize() { let localizer = crate::localize::localizer(); @@ -56,18 +59,18 @@ fn main() { app.connect_activate(|app| { load_css(); - let (tx, mut rx) = mpsc::channel::>(100); + let (tx, rx) = MainContext::channel(Priority::default()); let wayland_tx = wayland::spawn_workspaces(tx.clone()); let window = CosmicWorkspacesWindow::new(app); TX.set(wayland_tx).unwrap(); - let _ = glib::MainContext::default().spawn_local(async move { - while let Some(workspace_list) = rx.recv().await { - // TODO update the model with the new workspace list - } - }); + rx.attach(None, glib::clone!(@weak window => @default-return glib::prelude::Continue(true), move |workspace_event| { + window.set_workspaces(workspace_event); + glib::prelude::Continue(true) + })); + window.show(); }); app.run(); diff --git a/applets/cosmic-applet-workspaces/src/utils.rs b/applets/cosmic-applet-workspaces/src/utils.rs index 7dd7f3f8..1e99418e 100644 --- a/applets/cosmic-applet-workspaces/src/utils.rs +++ b/applets/cosmic-applet-workspaces/src/utils.rs @@ -5,11 +5,12 @@ use std::path::PathBuf; use gtk4::glib; use std::future::Future; -pub type Activate = u32; -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Workspace { - pub(crate) id: u32, - pub(crate) active: bool, +pub type Activate = String; + +#[derive(Debug, Clone)] +pub enum WorkspaceEvent { + Activate(String), + Scroll(f64), } pub fn data_path() -> PathBuf { diff --git a/applets/cosmic-applet-workspaces/src/wayland.rs b/applets/cosmic-applet-workspaces/src/wayland.rs index 0bced8dc..06485485 100644 --- a/applets/cosmic-applet-workspaces/src/wayland.rs +++ b/applets/cosmic-applet-workspaces/src/wayland.rs @@ -1,52 +1,400 @@ -use crate::utils::{Activate, Workspace}; +use crate::{ + utils::{Activate, WorkspaceEvent}, + wayland::generated::client::zext_workspace_manager_v1::ZextWorkspaceManagerV1, + wayland_source::WaylandSource, +}; +use cosmic_panel_config::config::CosmicPanelConfig; +use gtk4::glib; use std::{ - num::ParseIntError, - os::unix::prelude::RawFd, - sync::{Arc, Mutex}, + collections::HashMap, env, hash::Hash, mem, os::unix::net::UnixStream, path::PathBuf, + sync::Arc, time::Duration, }; use tokio::sync::mpsc; -use wayland_client::{protocol::wl_registry, Display, GlobalManager}; +use wayland_backend::client::ObjectData; +use wayland_client::{ + event_created_child, + protocol::{ + wl_output::{self, WlOutput}, + wl_registry, + }, + ConnectError, Proxy, +}; +use wayland_client::{Connection, Dispatch, QueueHandle}; + +/// Generated protocol definitions mod generated { - // The generated code tends to trigger a lot of warnings - // so we isolate it into a very permissive module #![allow(dead_code, non_camel_case_types, unused_unsafe, unused_variables)] #![allow(non_upper_case_globals, non_snake_case, unused_imports)] + #![allow(missing_docs, clippy::all)] pub mod client { - // These imports are used by the generated code - pub(crate) use wayland_commons::map::{Object, ObjectMetadata}; - pub(crate) use wayland_commons::smallvec; - pub(crate) use wayland_commons::wire::{Argument, ArgumentType, Message, MessageDesc}; - pub(crate) use wayland_commons::{Interface, MessageGroup}; - pub(crate) use wayland_client::protocol::wl_output; - pub(crate) use wayland_client::sys; - pub(crate) use wayland_client::{AnonymousObject, Main, Proxy, ProxyMap}; - include!(concat!(env!("OUT_DIR"), "/ext_workspace.rs")); + //! Client-side API of this protocol + use wayland_client; + use wayland_client::protocol::*; + + pub mod __interfaces { + use wayland_client::protocol::__interfaces::*; + wayland_scanner::generate_interfaces!("src/ext-workspace-unstable-v1.xml"); + } + use self::__interfaces::*; + + wayland_scanner::generate_client_code!("src/ext-workspace-unstable-v1.xml"); } } -pub fn spawn_workspaces(tx: mpsc::Sender>) -> mpsc::Sender { - let (workspaces_tx, mut workspaces_rx) = mpsc::channel(100); - if let Ok(display) = std::env::var("HOST_WAYLAND_DISPLAY") +use generated::client::zext_workspace_manager_v1; + +use self::generated::client::{ + zext_workspace_group_handle_v1::{self, ZextWorkspaceGroupHandleV1}, + zext_workspace_handle_v1::{self, ZextWorkspaceHandleV1}, +}; +use calloop::channel::*; + +pub fn spawn_workspaces(tx: glib::Sender) -> SyncSender { + let (workspaces_tx, mut workspaces_rx) = calloop::channel::sync_channel(100); + + if let Ok(Ok(conn)) = std::env::var("HOST_WAYLAND_DISPLAY") .map_err(anyhow::Error::msg) - .and_then(|fd| Display::connect_to_name(fd).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 mut event_queue = display.create_event_queue(); - let attached_display = display.attach(event_queue.token()); - let globals = GlobalManager::new(&attached_display); - dbg!(event_queue.sync_roundtrip(&mut (), |_, _, _| unreachable!())); + let output = CosmicPanelConfig::load_from_env() + .unwrap_or_default() + .output; + 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(); - println!("Globals: "); - for (name, interface, version) in globals.list() { - println!("{}: {} (version {})", name, interface, version); + 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(), + configured_output: output, + expected_output: None, + tx, + running: true, + }; + let loop_handle = event_loop.handle(); + loop_handle.insert_source(workspaces_rx, |e, _, state| { + match e { + Event::Msg(WorkspaceEvent::Activate(id)) => { + if let Some(w) = state + .workspace_groups + .iter() + .find_map(|g| g.workspaces.iter().find(|w| w.name == id)) + { + w.workspace_handle.activate(); + state.workspace_manager.as_ref().unwrap().commit(); + } + } + Event::Msg(WorkspaceEvent::Scroll(v)) => { + if let Some((w_g, w_i)) = state + .workspace_groups + .iter() + .enumerate() + .find_map(|(g_i, g)| { + g.workspaces + .iter() + .position(|w| w.state == 0) + .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 { + 0 + } else { + w_i.wrapping_add(1) + } + } else { + if w_i == 0 { + max_w + } else { + w_i.wrapping_sub(1) + } + }; + if let Some(w) = w_g.workspaces.get(d_i) { + w.workspace_handle.activate(); + state.workspace_manager.as_ref().unwrap().commit(); + } + } + } + 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(); + }, + } + + }).unwrap(); + while state.running { + event_loop + .dispatch(Duration::from_millis(16), &mut state) + .unwrap(); } }); } else { - eprintln!("ENV variable HOST_WAYLAND_SOCKET is missing. Exiting..."); + eprintln!("ENV variable HOST_WAYLAND_DISPLAY is missing. Exiting..."); std::process::exit(1); } workspaces_tx } + +#[derive(Debug, Clone)] +pub struct State { + running: bool, + tx: glib::Sender, + configured_output: String, + expected_output: Option, + workspace_manager: Option, + workspace_groups: Vec, +} + +impl State { + // XXX + 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(), w.state))) + } else { + None + } + }) + .flatten() + } +} + +#[derive(Debug, Clone)] +struct WorkspaceGroup { + workspace_group_handle: ZextWorkspaceGroupHandleV1, + output: Option, + workspaces: Vec, +} + +#[derive(Debug, Clone)] +struct Workspace { + workspace_handle: ZextWorkspaceHandleV1, + name: String, + coordinates: Vec, + state: u32, +} + +impl Dispatch for State { + fn event( + &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[..] { + "zext_workspace_manager_v1" => { + let workspace_manager = registry + .bind::( + name, + 1, + qh, + (), + ) + .unwrap(); + self.workspace_manager = Some(workspace_manager); + } + "wl_output" => { + registry.bind::(name, 1, qh, ()).unwrap(); + } + _ => {} + } + } + } +} + +impl Dispatch for State { + fn event( + &mut self, + _: &zext_workspace_manager_v1::ZextWorkspaceManagerV1, + event: zext_workspace_manager_v1::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + match event { + zext_workspace_manager_v1::Event::WorkspaceGroup { workspace_group } => { + self.workspace_groups.push(WorkspaceGroup { + workspace_group_handle: workspace_group, + output: None, + workspaces: Vec::new(), + }); + } + zext_workspace_manager_v1::Event::Done => { + let _ = self.tx.send(self.clone()); + } + zext_workspace_manager_v1::Event::Finished => { + self.workspace_manager.take(); + } + } + // wl_compositor has no event + } + + event_created_child!(State, ZextWorkspaceManagerV1, [ + 0 => (ZextWorkspaceGroupHandleV1, ()) + ]); +} + +impl Dispatch for State { + fn event( + &mut self, + group: &ZextWorkspaceGroupHandleV1, + event: zext_workspace_group_handle_v1::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + match event { + zext_workspace_group_handle_v1::Event::OutputEnter { output } => { + if let Some(group) = self + .workspace_groups + .iter_mut() + .find(|g| &g.workspace_group_handle == group) + { + group.output = Some(output); + } + } + zext_workspace_group_handle_v1::Event::OutputLeave { output } => { + if let Some(group) = self.workspace_groups.iter_mut().find(|g| { + &g.workspace_group_handle == group && g.output.as_ref() == Some(&output) + }) { + group.output = None; + } + } + zext_workspace_group_handle_v1::Event::Workspace { workspace } => { + if let Some(group) = self + .workspace_groups + .iter_mut() + .find(|g| &g.workspace_group_handle == group) + { + group.workspaces.push(Workspace { + workspace_handle: workspace, + name: String::new(), + coordinates: Vec::new(), + state: 4, + }) + } + } + zext_workspace_group_handle_v1::Event::Remove => { + if let Some(group) = self + .workspace_groups + .iter() + .position(|g| &g.workspace_group_handle == group) + { + self.workspace_groups.remove(group); + } + } + } + } + + event_created_child!(State, ZextWorkspaceGroupHandleV1, [ + 2 => (ZextWorkspaceHandleV1, ()) + ]); +} + +impl Dispatch for State { + fn event( + &mut self, + workspace: &ZextWorkspaceHandleV1, + event: zext_workspace_handle_v1::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + match event { + zext_workspace_handle_v1::Event::Name { name } => { + if let Some(w) = self.workspace_groups.iter_mut().find_map(|g| { + g.workspaces + .iter_mut() + .find(|w| &w.workspace_handle == workspace) + }) { + w.name = name; + } + } + zext_workspace_handle_v1::Event::Coordinates { coordinates } => { + if let Some(w) = self.workspace_groups.iter_mut().find_map(|g| { + g.workspaces + .iter_mut() + .find(|w| &w.workspace_handle == workspace) + }) { + w.coordinates = coordinates; + } + } + zext_workspace_handle_v1::Event::State { state } => { + if let Some(w) = self.workspace_groups.iter_mut().find_map(|g| { + g.workspaces + .iter_mut() + .find(|w| &w.workspace_handle == workspace) + }) { + if state.len() == 4 { + // XXX is it little endian?? + w.state = u32::from_le_bytes(state.try_into().unwrap()); + } else { + w.state = 3; + } + } + } + zext_workspace_handle_v1::Event::Remove => { + if let Some((g, w_i)) = self.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( + &mut self, + o: &WlOutput, + e: wl_output::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + match e { + wl_output::Event::Name { name } if name == self.configured_output => { + self.expected_output.replace(o.clone()); + } + _ => {} // ignored + } + } +} diff --git a/applets/cosmic-applet-workspaces/src/wayland_source.rs b/applets/cosmic-applet-workspaces/src/wayland_source.rs new file mode 100644 index 00000000..028c9e56 --- /dev/null +++ b/applets/cosmic-applet-workspaces/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-workspaces/src/window/mod.rs b/applets/cosmic-applet-workspaces/src/window/mod.rs index 4b0035b7..220698ab 100644 --- a/applets/cosmic-applet-workspaces/src/window/mod.rs +++ b/applets/cosmic-applet-workspaces/src/window/mod.rs @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MPL-2.0-only -use crate::{fl, utils::Activate, workspace_list::WorkspaceList}; +use crate::{fl, utils::Activate, wayland::State, workspace_list::WorkspaceList}; use cascade::cascade; use cosmic_panel_config::config::CosmicPanelConfig; use gtk4::{ @@ -9,7 +9,6 @@ use gtk4::{ prelude::*, subclass::prelude::*, }; -use tokio::sync::mpsc; mod imp; @@ -32,7 +31,7 @@ impl CosmicWorkspacesWindow { ..set_height_request(1); ..set_decorated(false); ..set_resizable(false); - ..set_title(Some(&fl!("cosmic-app-list"))); + ..set_title(Some(&fl!("cosmic-applet-workspaces"))); ..add_css_class("transparent"); }; let config = CosmicPanelConfig::load_from_env().unwrap_or_default(); @@ -43,4 +42,9 @@ impl CosmicWorkspacesWindow { self_ } + + pub fn set_workspaces(&self, workspaces: State) { + let imp = imp::CosmicWorkspacesWindow::from_instance(&self); + imp.inner.get().unwrap().set_workspaces(workspaces); + } } diff --git a/applets/cosmic-applet-workspaces/src/workspace_button/mod.rs b/applets/cosmic-applet-workspaces/src/workspace_button/mod.rs index 22dea99b..4b8d1c03 100644 --- a/applets/cosmic-applet-workspaces/src/workspace_button/mod.rs +++ b/applets/cosmic-applet-workspaces/src/workspace_button/mod.rs @@ -1,6 +1,6 @@ mod imp; -use crate::{workspace_object::WorkspaceObject, Activate, TX}; +use crate::{utils::WorkspaceEvent, workspace_object::WorkspaceObject, Activate, TX}; use glib::Object; use gtk4::{glib, prelude::*, subclass::prelude::*, ToggleButton}; @@ -28,12 +28,25 @@ impl WorkspaceButton { let old_button = imp.button.take(); self.remove(&old_button); + let is_active = obj.active() == 0; let id = obj.id(); - let new_button = ToggleButton::with_label(&format!("{}", id)); - new_button.set_active(obj.active()); + let new_button = ToggleButton::with_label(&id); + new_button.set_sensitive(!is_active); + if obj.active() == 0 { + new_button.add_css_class("active"); + } else if obj.active() == 1 { + new_button.add_css_class("alert"); + } else { + new_button.add_css_class("inactive"); + } self.append(&new_button); new_button.connect_clicked(move |_| { - let _ = TX.get().unwrap().send(id); + let id_clone = id.clone(); + if !is_active { + let _ = TX.get() + .unwrap() + .send(WorkspaceEvent::Activate(id_clone)); + } }); imp.button.replace(new_button); diff --git a/applets/cosmic-applet-workspaces/src/workspace_list/imp.rs b/applets/cosmic-applet-workspaces/src/workspace_list/imp.rs index 634c1714..94f4da3e 100644 --- a/applets/cosmic-applet-workspaces/src/workspace_list/imp.rs +++ b/applets/cosmic-applet-workspaces/src/workspace_list/imp.rs @@ -2,7 +2,7 @@ use cosmic_panel_config::config::CosmicPanelConfig; use gtk4::subclass::prelude::*; -use gtk4::{gio, glib}; +use gtk4::{gio, glib, EventControllerScroll}; use gtk4::{Box, ListView}; use once_cell::sync::OnceCell; use tokio::sync::mpsc; diff --git a/applets/cosmic-applet-workspaces/src/workspace_list/mod.rs b/applets/cosmic-applet-workspaces/src/workspace_list/mod.rs index e7c031ef..0fe7b019 100644 --- a/applets/cosmic-applet-workspaces/src/workspace_list/mod.rs +++ b/applets/cosmic-applet-workspaces/src/workspace_list/mod.rs @@ -1,10 +1,16 @@ // SPDX-License-Identifier: MPL-2.0-only use crate::utils::Activate; +use crate::utils::WorkspaceEvent; +use crate::wayland::State; use crate::workspace_button::WorkspaceButton; use crate::workspace_object::WorkspaceObject; +use crate::TX; use cascade::cascade; use cosmic_panel_config::config::CosmicPanelConfig; +use gtk4::builders::EventControllerScrollBuilder; +use gtk4::EventControllerScrollFlags; +use gtk4::Inhibit; use gtk4::ListView; use gtk4::Orientation; use gtk4::SignalListItemFactory; @@ -39,15 +45,52 @@ impl WorkspaceList { fn layout(&self) { let imp = imp::WorkspaceList::from_instance(self); + let anchor = imp.config.get().unwrap().anchor; + let list_view = cascade! { ListView::default(); - ..set_orientation(Orientation::Horizontal); + ..set_orientation(anchor.into()); ..add_css_class("transparent"); }; self.append(&list_view); + + let flags = EventControllerScrollFlags::BOTH_AXES; + + let scroll_controller = EventControllerScrollBuilder::new() + .flags(flags.union(EventControllerScrollFlags::DISCRETE)) + .build(); + + scroll_controller.connect_scroll(|_, dx, dy| { + let _ = TX.get() + .unwrap() + .send(WorkspaceEvent::Scroll(dx + dy)); + Inhibit::default() + }); + + list_view.add_controller(&scroll_controller); imp.list_view.set(list_view).unwrap(); } + pub fn set_workspaces(&self, workspaces: State) { + let imp = imp::WorkspaceList::from_instance(&self); + let model = imp.model.get().unwrap(); + + let model_len = model.n_items(); + let new_results: Vec = workspaces + .workspace_list() + .into_iter() + .filter_map(|w| { + // don't include hidden workspaces + if w.1 != 2 { + Some(WorkspaceObject::from_id_active(w.0, w.1).upcast()) + } else { + None + } + }) + .collect(); + model.splice(0, model_len, &new_results[..]); + } + fn setup_model(&self) { let imp = imp::WorkspaceList::from_instance(self); let model = gio::ListStore::new(WorkspaceObject::static_type()); @@ -74,7 +117,7 @@ impl WorkspaceList { .item() .expect("The item has to exist.") .downcast::() - .expect("The item has to be a `DockObject`"); + .expect("The item has to be a `WorkspaceObject`"); let workspace_button = list_item .child() .expect("The list item child needs to exist.") diff --git a/applets/cosmic-applet-workspaces/src/workspace_object/imp.rs b/applets/cosmic-applet-workspaces/src/workspace_object/imp.rs index 5de4e464..c9930794 100644 --- a/applets/cosmic-applet-workspaces/src/workspace_object/imp.rs +++ b/applets/cosmic-applet-workspaces/src/workspace_object/imp.rs @@ -1,11 +1,10 @@ // SPDX-License-Identifier: MPL-2.0-only -use std::cell::Cell; +use std::cell::{Cell, RefCell}; use glib::{ParamFlags, ParamSpec, Value}; use gtk4::gdk::glib::ParamSpecBoolean; -use gtk4::glib; -use gtk4::glib::ParamSpecUInt; +use gtk4::glib::{self, ParamSpecString, ParamSpecUInt}; use gtk4::prelude::*; use gtk4::subclass::prelude::*; use once_cell::sync::Lazy; @@ -13,8 +12,8 @@ use once_cell::sync::Lazy; // Object holding the state #[derive(Default)] pub struct WorkspaceObject { - pub(crate) id: Cell, - pub(crate) active: Cell, + pub(crate) id: RefCell, + pub(crate) active: Cell, } // The central trait for subclassing a GObject @@ -30,27 +29,25 @@ impl ObjectImpl for WorkspaceObject { fn properties() -> &'static [ParamSpec] { static PROPERTIES: Lazy> = Lazy::new(|| { vec![ - ParamSpecUInt::new( + ParamSpecString::new( // Name "id", // Nickname "id", // Short description "id", - // Minimum value - u32::MIN, - // Maximum value - u32::MAX, // Default value - 0, + None, // The property can be read and written to ParamFlags::READWRITE, ), - ParamSpecBoolean::new( + ParamSpecUInt::new( "active", "active", "Indicates whether workspace is active", - false, + 0, + 4, + 0, ParamFlags::READWRITE, ), ] @@ -74,7 +71,7 @@ impl ObjectImpl for WorkspaceObject { fn property(&self, _obj: &Self::Type, _id: usize, pspec: &ParamSpec) -> Value { match pspec.name() { - "id" => self.id.get().to_value(), + "id" => self.id.borrow().to_value(), "active" => self.active.get().to_value(), _ => unimplemented!(), } diff --git a/applets/cosmic-applet-workspaces/src/workspace_object/mod.rs b/applets/cosmic-applet-workspaces/src/workspace_object/mod.rs index 7b171a94..83a178ee 100644 --- a/applets/cosmic-applet-workspaces/src/workspace_object/mod.rs +++ b/applets/cosmic-applet-workspaces/src/workspace_object/mod.rs @@ -13,15 +13,18 @@ impl WorkspaceObject { glib::Object::new(&[]).unwrap() } - pub fn from_id_active(id: u32, active: bool) -> Self { + pub fn from_id_active(id: String, active: u32) -> Self { glib::Object::new(&[("id", &id), ("active", &active)]).unwrap() } - pub fn id(&self) -> u32 { - imp::WorkspaceObject::from_instance(&self).id.get() + pub fn id(&self) -> String { + imp::WorkspaceObject::from_instance(&self) + .id + .borrow() + .clone() } - pub fn active(&self) -> bool { + pub fn active(&self) -> u32 { imp::WorkspaceObject::from_instance(&self).active.get() } } diff --git a/applets/cosmic-panel-button/src/apps_window/mod.rs b/applets/cosmic-panel-button/src/apps_window/mod.rs index 53aeacf1..d1a1f8e6 100644 --- a/applets/cosmic-panel-button/src/apps_window/mod.rs +++ b/applets/cosmic-panel-button/src/apps_window/mod.rs @@ -34,7 +34,9 @@ impl CosmicPanelAppButtonWindow { ..add_css_class("root_window"); }; - if let Some(apps_desktop_info) = DesktopAppInfo::new(&format!("{}.desktop", app_desktop_file_name)) { + if let Some(apps_desktop_info) = + DesktopAppInfo::new(&format!("{}.desktop", app_desktop_file_name)) + { let app_button = cascade! { Button::new(); ..add_css_class("apps"); @@ -54,16 +56,16 @@ impl CosmicPanelAppButtonWindow { container.append(&image); app_button.set_child(Some(&container)); - dbg!(apps_desktop_info.string("Exec").unwrap().as_str()); + let app_id = app_desktop_file_name.to_string(); app_button.connect_clicked(move |_| { let _ = Command::new("xdg-shell-wrapper") .env_remove("WAYLAND_SOCKET") - .arg(apps_desktop_info.string("Exec").unwrap().as_str()) + .arg(&app_id) .spawn(); }); self_.set_child(Some(&app_button)); } else { - panic!("Requested application is not installed"); + panic!("{} is not installed", app_desktop_file_name); } self_ diff --git a/applets/cosmic-panel-button/src/main.rs b/applets/cosmic-panel-button/src/main.rs index ecbcbf37..eb2b243e 100644 --- a/applets/cosmic-panel-button/src/main.rs +++ b/applets/cosmic-panel-button/src/main.rs @@ -44,7 +44,14 @@ fn main() { localize(); gio::resources_register_include!("compiled.gresource").unwrap(); let app = gtk4::Application::new(None, ApplicationFlags::default()); - app.add_main_option("id", glib::Char::from(b'i'), glib::OptionFlags::NONE, glib::OptionArg::String, "id of the launched application", None); + app.add_main_option( + "id", + glib::Char::from(b'i'), + glib::OptionFlags::NONE, + glib::OptionArg::String, + "id of the launched application", + None, + ); app.connect_handle_local_options(|_app, args| { if let Ok(Some(id)) = args.lookup::("id") { ID.set(id).unwrap();