Merge pull request #22 from pop-os/app-list-toplevel-dev

App list toplevel dev
This commit is contained in:
Ashley Wulber 2022-07-20 18:17:06 -04:00 committed by GitHub
commit 9b97404fa6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 1314 additions and 743 deletions

49
Cargo.lock generated
View file

@ -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",

View file

@ -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" }

View file

@ -10,3 +10,4 @@ Keywords=Gnome;GTK;
Icon=com.system76.CosmicAppList.svg
StartupNotify=true
NoDisplay=true
HostWaylandDisplay=true

View file

@ -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<Event>) -> 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);

View file

@ -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: &gtk4::Application, tx: mpsc::Sender<Event>) -> Self {
pub fn new(app: &gtk4::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::<gtk4::Window>();
let action_quit = gio::SimpleAction::new("quit", None);

View file

@ -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<TopLevelFilter>,
}
impl AppListConfig {
/// load config with the provided name
pub fn load() -> anyhow::Result<AppListConfig> {
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))
}
}

View file

@ -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<RefCell<gtk4::Box>>,
pub popover: Rc<RefCell<gtk4::Popover>>,
pub popover_menu: Rc<RefCell<Option<DockPopover>>>,
pub tx: OnceCell<Sender<Event>>,
pub icon_size: Rc<Cell<u32>>,
}

View file

@ -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<Event>, 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);

View file

@ -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<RefCell<Option<SignalHandlerId>>>,
pub popover_menu_index: Rc<Cell<Option<u32>>>,
pub position: Rc<Cell<PanelAnchor>>,
pub tx: OnceCell<mpsc::Sender<Event>>,
pub config: OnceCell<CosmicPanelConfig>
pub config: OnceCell<CosmicPanelConfig>,
}
#[glib::object_subclass]

View file

@ -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<Event>, 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<String>>(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::<WaylandSeat>().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) {

View file

@ -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<Self> {
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) {

View file

@ -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<RefCell<Button>>,
//TODO figure out how to use lifetimes with glib::wrapper! macro
pub dock_object: Rc<RefCell<Option<DockObject>>>,
pub tx: OnceCell<Sender<Event>>,
}
#[glib::object_subclass]

View file

@ -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<Event>) -> 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::<BoxedWindowList>("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::<bool>("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::<BoxedWindowList>("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();
}),
);

View file

@ -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<mpsc::Sender<Event>> = OnceCell::new();
static TX: OnceCell<glib::Sender<AppListEvent>> = OnceCell::new();
static WAYLAND_TX: OnceCell<SyncSender<ToplevelEvent>> = 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<u32> = None;
while let Some(item) = active_app_model.item(cur) {
if let Ok(cur_dock_object) = item.downcast::<DockObject>() {
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<u32> = None;
while let Some(item) = active_app_model.item(cur) {
if let Ok(cur_dock_object) = item.downcast::<DockObject>() {
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<u32> = None;
while let Some(item) = saved_app_model.item(cur) {
if let Ok(cur_dock_object) = item.downcast::<DockObject>() {
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<String, BoxedWindowList>, 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<BoxedWindowList> =
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::<DockObject>() {
if let Some(cur_app_info) =
dock_obj.property::<Option<DesktopAppInfo>>("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<u32> = None;
while let Some(item) = saved_app_model.item(cur) {
if let Ok(cur_dock_object) = item.downcast::<DockObject>() {
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<glib::Object> = 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<String, BoxedWindowList>, 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<BoxedWindowList> =
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::<DockObject>() {
if let Some(cur_app_info) =
dock_obj.property::<Option<DesktopAppInfo>>("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<glib::Object> = 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<String, BoxedWindowList>, 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<BoxedWindowList> =
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::<DockObject>() {
if let Some(cur_app_info) =
dock_obj.property::<Option<DesktopAppInfo>>("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<glib::Object> = 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<String, BoxedWindowList>, 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<BoxedWindowList> =
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::<DockObject>() {
if let Some(cur_app_info) =
dock_obj.property::<Option<DesktopAppInfo>>("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<glib::Object> = 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();

View file

@ -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;

View file

@ -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<Toplevel>),
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<Item>);
pub struct BoxedWindowList(pub Vec<Toplevel>);
pub fn data_path() -> PathBuf {
let mut path = glib::user_data_dir();

View file

@ -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<ToplevelEvent> {
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::<PathBuf>::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::<State>::try_new().unwrap();
let loop_handle = event_loop.handle();
let event_queue = conn.new_event_queue::<State>();
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<String>,
expected_output: Option<WlOutput>,
workspace_manager: Option<ZcosmicWorkspaceManagerV1>,
workspace_groups: Vec<WorkspaceGroup>,
toplevel_info: Option<ZcosmicToplevelInfoV1>,
toplevel_manager: Option<ZcosmicToplevelManagerV1>,
toplevels: Vec<Toplevel>,
seats: Vec<WlSeat>,
}
impl State {
pub fn workspace_list(&self) -> impl Iterator<Item = (String, u32)> + '_ {
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<zcosmic_toplevel_handle_v1::State>,
pub output: Option<WlOutput>,
pub workspace: Option<ZcosmicWorkspaceHandleV1>,
}
#[derive(Debug, Clone)]
struct WorkspaceGroup {
workspace_group_handle: ZcosmicWorkspaceGroupHandleV1,
output: Option<WlOutput>,
workspaces: Vec<Workspace>,
}
#[derive(Debug, Clone)]
struct Workspace {
workspace_handle: ZcosmicWorkspaceHandleV1,
name: String,
coordinates: Vec<u32>,
states: Vec<zcosmic_workspace_handle_v1::State>,
}
impl Dispatch<wl_registry::WlRegistry, ()> for State {
fn event(
state: &mut Self,
registry: &wl_registry::WlRegistry,
event: wl_registry::Event,
_: &(),
_: &Connection,
qh: &QueueHandle<Self>,
) {
if let wl_registry::Event::Global {
name,
interface,
version,
} = event
{
match &interface[..] {
"zcosmic_toplevel_info_v1" => {
let ti = registry
.bind::<ZcosmicToplevelInfoV1, _, _>(name, 1, qh, ())
.unwrap();
state.toplevel_info = Some(ti);
}
"zcosmic_toplevel_manager_v1" => {
let tm = registry
.bind::<ZcosmicToplevelManagerV1, _, _>(name, 1, qh, ())
.unwrap();
state.toplevel_manager = Some(tm);
}
"zcosmic_workspace_manager_v1" => {
let workspace_manager = registry
.bind::<ZcosmicWorkspaceManagerV1, _, _>(name, 1, qh, ())
.unwrap();
state.workspace_manager = Some(workspace_manager);
}
"wl_seat" => {
registry.bind::<WlSeat, _, _>(name, 1, qh, ()).unwrap();
}
"wl_output" => {
registry.bind::<WlOutput, _, _>(name, 1, qh, ()).unwrap();
}
_ => {}
}
}
}
}
impl Dispatch<ZcosmicToplevelInfoV1, ()> for State {
fn event(
state: &mut Self,
_: &ZcosmicToplevelInfoV1,
event: <ZcosmicToplevelInfoV1 as Proxy>::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
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<ZcosmicToplevelManagerV1, ()> for State {
fn event(
_: &mut Self,
_: &ZcosmicToplevelManagerV1,
event: <ZcosmicToplevelManagerV1 as Proxy>::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
match event {
zcosmic_toplevel_manager_v1::Event::Capabilities { .. } => {
// TODO capabilities affect what is shown to user in applet
}
_ => {}
}
}
}
impl Dispatch<ZcosmicToplevelHandleV1, ()> for State {
fn event(
state: &mut Self,
p: &ZcosmicToplevelHandleV1,
event: <ZcosmicToplevelHandleV1 as Proxy>::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
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<ZcosmicWorkspaceManagerV1, ()> for State {
fn event(
state: &mut Self,
_: &ZcosmicWorkspaceManagerV1,
event: zcosmic_workspace_manager_v1::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
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<ZcosmicWorkspaceGroupHandleV1, ()> for State {
fn event(
state: &mut Self,
group: &ZcosmicWorkspaceGroupHandleV1,
event: zcosmic_workspace_group_handle_v1::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
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<ZcosmicWorkspaceHandleV1, ()> for State {
fn event(
state: &mut Self,
workspace: &ZcosmicWorkspaceHandleV1,
event: zcosmic_workspace_handle_v1::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
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<WlOutput, ()> for State {
fn event(
state: &mut Self,
o: &WlOutput,
e: wl_output::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
match e {
wl_output::Event::Name { name } if Some(&name) == state.configured_output.as_ref() => {
state.expected_output.replace(o.clone());
}
_ => {} // ignored
}
}
}
impl Dispatch<WlSeat, ()> for State {
fn event(
state: &mut Self,
seat: &WlSeat,
_: wl_seat::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
if state.seats.iter().find(|s| s == &seat).is_none() {
state.seats.push(seat.clone());
}
}
}

View file

@ -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<D> {
queue: EventQueue<D>,
fd: Generic<RawFd>,
read_guard: Option<ReadEventsGuard>,
}
impl<D> WaylandSource<D> {
/// Wrap an [`EventQueue`] as a [`WaylandSource`].
pub fn new(queue: EventQueue<D>) -> Result<WaylandSource<D>, 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<D> {
&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<D>) -> Result<RegistrationToken, InsertError<Self>>
where
D: 'static,
{
handle.insert_source(self, |_, queue, data| queue.dispatch_pending(data))
}
}
impl<D> EventSource for WaylandSource<D> {
type Event = ();
/// The underlying event queue.
///
/// You should call [`EventQueue::dispatch_pending`] inside your callback using this queue.
type Metadata = EventQueue<D>;
type Ret = Result<usize, DispatchError>;
type Error = calloop::Error;
fn process_events<F>(
&mut self,
readiness: Readiness,
token: Token,
mut callback: F,
) -> Result<PostAction, Self::Error>
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<F>(&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<F>(&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<D> WaylandSource<D> {
/// Loop over the callback until all pending messages have been dispatched.
fn loop_callback_pending<F>(queue: &mut EventQueue<D>, callback: &mut F) -> io::Result<()>
where
F: FnMut((), &mut EventQueue<D>) -> Result<usize, DispatchError>,
{
// 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<D>) -> io::Result<ReadEventsGuard> {
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()
}
})
}
}

View file

@ -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");

View file

@ -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"

View file

@ -1,306 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<protocol name="ext_workspace_unstable_v1">
<copyright>
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.
</copyright>
<interface name="zext_workspace_manager_v1" version="1">
<description summary="list and control workspaces">
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.
</description>
<event name="workspace_group">
<description summary="a workspace group has been created">
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.
</description>
<arg name="workspace_group" type="new_id" interface="zext_workspace_group_handle_v1"/>
</event>
<request name="commit">
<description summary="all requests about the workspaces have been sent">
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.
</description>
</request>
<event name="done">
<description summary="all information about the workspace groups has been sent">
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.
</description>
</event>
<event name="finished">
<description summary="the compositor has finished with the workspace_manager">
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.
</description>
</event>
<request name="stop">
<description summary="stop sending events">
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.
</description>
</request>
</interface>
<interface name="zext_workspace_group_handle_v1" version="1">
<description summary="a workspace group assigned to a set of outputs">
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.
</description>
<event name="output_enter">
<description summary="output assigned to workspace group">
This event is emitted whenever an output is assigned to the workspace
group.
</description>
<arg name="output" type="object" interface="wl_output"/>
</event>
<event name="output_leave">
<description summary="output removed from workspace group">
This event is emitted whenever an output is removed from the workspace
group.
</description>
<arg name="output" type="object" interface="wl_output"/>
</event>
<event name="workspace">
<description summary="workspace added to 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.
</description>
<arg name="workspace" type="new_id" interface="zext_workspace_handle_v1"/>
</event>
<event name="remove">
<description summary="this workspace group has been destroyed">
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.
</description>
</event>
<request name="create_workspace">
<description summary="create a new workspace">
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.
</description>
<arg name="workspace" type="string"/>
</request>
<request name="destroy" type="destructor">
<description summary="destroy the zext_workspace_handle_v1 object">
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.
</description>
</request>
</interface>
<interface name="zext_workspace_handle_v1" version="1">
<description summary="a workspace handing a group of surfaces">
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).
</description>
<event name="name">
<description summary="workspace name changed">
This event is emitted immediately after the zext_workspace_handle_v1 is
created and whenever the name of the workspace changes.
</description>
<arg name="name" type="string"/>
</event>
<event name="coordinates">
<description summary="workspace coordinates changed">
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.
</description>
<arg name="coordinates" type="array"/>
</event>
<event name="state">
<description summary="the state of the workspace changed">
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.
</description>
<arg name="state" type="array"/>
</event>
<enum name="state">
<description summary="types of states on the workspace">
The different states that a workspace can have.
</description>
<entry name="active" value="0" summary="the workspace is active"/>
<entry name="urgent" value="1" summary="the workspace requests attention"/>
<entry name="hidden" value="2">
<description summary="the workspace is not visible">
The workspace is not visible in its workspace group, and clients
attempting to visualize the compositor workspace state should not
display such workspaces.
</description>
</entry>
</enum>
<event name="remove">
<description summary="this workspace has been destroyed">
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.
</description>
</event>
<request name="destroy" type="destructor">
<description summary="destroy the zext_workspace_handle_v1 object">
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.
</description>
</request>
<request name="activate">
<description summary="activate the workspace">
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.
</description>
</request>
<request name="deactivate">
<description summary="activate the workspace">
Request that this workspace be deactivated.
There is no guarantee the workspace will be actually deactivated.
</description>
</request>
<request name="remove">
<description summary="remove the workspace">
Request that this workspace be removed.
There is no guarantee the workspace will be actually removed.
</description>
</request>
</interface>
</protocol>

View file

@ -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<State>) -> SyncSender<WorkspaceEvent> {
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<State>) -> SyncSender<WorkspaceEvent> {
}
}
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<State>) -> SyncSender<WorkspaceEvent> {
}
});
} 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<wl_registry::WlRegistry, ()> for State {
fn event(
&mut self,
state: &mut Self,
registry: &wl_registry::WlRegistry,
event: wl_registry::Event,
_: &(),
@ -201,14 +205,9 @@ impl Dispatch<wl_registry::WlRegistry, ()> for State {
match &interface[..] {
"zcosmic_workspace_manager_v1" => {
let workspace_manager = registry
.bind::<ZcosmicWorkspaceManagerV1, _, _>(
name,
1,
qh,
(),
)
.bind::<ZcosmicWorkspaceManagerV1, _, _>(name, 1, qh, ())
.unwrap();
self.workspace_manager = Some(workspace_manager);
state.workspace_manager = Some(workspace_manager);
}
"wl_output" => {
registry.bind::<WlOutput, _, _>(name, 1, qh, ()).unwrap();
@ -221,7 +220,7 @@ impl Dispatch<wl_registry::WlRegistry, ()> for State {
impl Dispatch<ZcosmicWorkspaceManagerV1, ()> for State {
fn event(
&mut self,
state: &mut Self,
_: &ZcosmicWorkspaceManagerV1,
event: zcosmic_workspace_manager_v1::Event,
_: &(),
@ -230,26 +229,28 @@ impl Dispatch<ZcosmicWorkspaceManagerV1, ()> 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<ZcosmicWorkspaceManagerV1, ()> for State {
impl Dispatch<ZcosmicWorkspaceGroupHandleV1, ()> for State {
fn event(
&mut self,
state: &mut Self,
group: &ZcosmicWorkspaceGroupHandleV1,
event: zcosmic_workspace_group_handle_v1::Event,
_: &(),
@ -271,7 +272,7 @@ impl Dispatch<ZcosmicWorkspaceGroupHandleV1, ()> 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<ZcosmicWorkspaceGroupHandleV1, ()> 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<ZcosmicWorkspaceGroupHandleV1, ()> 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<ZcosmicWorkspaceGroupHandleV1, ()> for State {
impl Dispatch<ZcosmicWorkspaceHandleV1, ()> for State {
fn event(
&mut self,
state: &mut Self,
workspace: &ZcosmicWorkspaceHandleV1,
event: zcosmic_workspace_handle_v1::Event,
_: &(),
@ -329,7 +330,7 @@ impl Dispatch<ZcosmicWorkspaceHandleV1, ()> 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<ZcosmicWorkspaceHandleV1, ()> 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<ZcosmicWorkspaceHandleV1, ()> for State {
impl Dispatch<WlOutput, ()> for State {
fn event(
&mut self,
state: &mut Self,
o: &WlOutput,
e: wl_output::Event,
_: &(),
@ -382,8 +396,8 @@ impl Dispatch<WlOutput, ()> for State {
_: &QueueHandle<Self>,
) {
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
}