feat: app list is displayed and updated
This commit is contained in:
parent
ceff811072
commit
8223c76361
17 changed files with 4317 additions and 1256 deletions
97
Cargo.lock
generated
97
Cargo.lock
generated
|
|
@ -230,19 +230,6 @@ dependencies = [
|
||||||
"system-deps",
|
"system-deps",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "calloop"
|
|
||||||
version = "0.10.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a22a6a8f622f797120d452c630b0ab12e1331a1a753e2039ce7868d4ac77b4ee"
|
|
||||||
dependencies = [
|
|
||||||
"log",
|
|
||||||
"nix 0.24.2",
|
|
||||||
"slotmap",
|
|
||||||
"thiserror",
|
|
||||||
"vec_map",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cascade"
|
name = "cascade"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
|
|
@ -300,39 +287,6 @@ version = "0.8.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
|
checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cosmic-app-list"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"anyhow",
|
|
||||||
"calloop",
|
|
||||||
"cascade",
|
|
||||||
"cosmic-panel-config",
|
|
||||||
"cosmic-protocols",
|
|
||||||
"futures",
|
|
||||||
"futures-util",
|
|
||||||
"gio",
|
|
||||||
"glib-build-tools",
|
|
||||||
"gsk4",
|
|
||||||
"gtk4",
|
|
||||||
"i18n-embed",
|
|
||||||
"i18n-embed-fl",
|
|
||||||
"libadwaita",
|
|
||||||
"libcosmic",
|
|
||||||
"log",
|
|
||||||
"nix 0.25.0",
|
|
||||||
"once_cell",
|
|
||||||
"pretty_env_logger",
|
|
||||||
"relm4-macros",
|
|
||||||
"ron 0.8.0",
|
|
||||||
"rust-embed",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"wayland-backend",
|
|
||||||
"wayland-client",
|
|
||||||
"xdg",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cosmic-applet-notifications"
|
name = "cosmic-applet-notifications"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
@ -420,18 +374,6 @@ dependencies = [
|
||||||
"xdg-shell-wrapper-config",
|
"xdg-shell-wrapper-config",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cosmic-protocols"
|
|
||||||
version = "0.1.0"
|
|
||||||
source = "git+https://github.com/pop-os/cosmic-protocols#3ff11df30ef551e1ccbdcb091930fe0d72266195"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags",
|
|
||||||
"wayland-backend",
|
|
||||||
"wayland-client",
|
|
||||||
"wayland-protocols",
|
|
||||||
"wayland-scanner",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cpufeatures"
|
name = "cpufeatures"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
|
|
@ -1148,12 +1090,6 @@ dependencies = [
|
||||||
"windows-sys",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "itoa"
|
|
||||||
version = "1.0.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
version = "0.3.59"
|
version = "0.3.59"
|
||||||
|
|
@ -1331,7 +1267,6 @@ dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"libc",
|
"libc",
|
||||||
"memoffset",
|
"memoffset",
|
||||||
"pin-utils",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1806,12 +1741,6 @@ dependencies = [
|
||||||
"semver",
|
"semver",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ryu"
|
|
||||||
version = "1.0.11"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "same-file"
|
name = "same-file"
|
||||||
version = "1.0.6"
|
version = "1.0.6"
|
||||||
|
|
@ -1877,17 +1806,6 @@ dependencies = [
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "serde_json"
|
|
||||||
version = "1.0.83"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "38dd04e3c8279e75b31ef29dbdceebfe5ad89f4d0937213c53f7d49d01b3d5a7"
|
|
||||||
dependencies = [
|
|
||||||
"itoa",
|
|
||||||
"ryu",
|
|
||||||
"serde",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_repr"
|
name = "serde_repr"
|
||||||
version = "0.1.9"
|
version = "0.1.9"
|
||||||
|
|
@ -1948,15 +1866,6 @@ version = "2.7.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8347046d4ebd943127157b94d63abb990fcf729dc4e9978927fdf4ac3c998d06"
|
checksum = "8347046d4ebd943127157b94d63abb990fcf729dc4e9978927fdf4ac3c998d06"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "slotmap"
|
|
||||||
version = "1.0.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e1e08e261d0e8f5c43123b7adf3e4ca1690d655377ac93a03b2c9d3e98de1342"
|
|
||||||
dependencies = [
|
|
||||||
"version_check",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "smallvec"
|
name = "smallvec"
|
||||||
version = "1.9.0"
|
version = "1.9.0"
|
||||||
|
|
@ -2166,12 +2075,6 @@ version = "1.0.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf"
|
checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "vec_map"
|
|
||||||
version = "0.8.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "version-compare"
|
name = "version-compare"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
members = [
|
members = [
|
||||||
"applets/cosmic-applet-notifications",
|
"applets/cosmic-applet-notifications",
|
||||||
"applets/cosmic-applet-status-area",
|
"applets/cosmic-applet-status-area",
|
||||||
"applets/cosmic-app-list",
|
|
||||||
"applets/cosmic-panel-button",
|
"applets/cosmic-panel-button",
|
||||||
"libcosmic-applet",
|
"libcosmic-applet",
|
||||||
]
|
]
|
||||||
|
|
@ -14,6 +13,7 @@ exclude = [
|
||||||
"applets/cosmic-applet-audio",
|
"applets/cosmic-applet-audio",
|
||||||
"applets/cosmic-applet-power",
|
"applets/cosmic-applet-power",
|
||||||
"applets/cosmic-applet-time",
|
"applets/cosmic-applet-time",
|
||||||
|
"applets/cosmic-app-list",
|
||||||
]
|
]
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
|
|
|
||||||
3677
applets/cosmic-app-list/Cargo.lock
generated
Normal file
3677
applets/cosmic-app-list/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -5,33 +5,33 @@ edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
[dependencies]
|
[dependencies]
|
||||||
cosmic-panel-config = {git = "https://github.com/pop-os/cosmic-panel", features = ["gtk4"] }
|
cctk = {git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit"}
|
||||||
cosmic-protocols = { git = "https://github.com/pop-os/cosmic-protocols", default-features = false, features = ["client"] }
|
cosmic-protocols = { git = "https://github.com/pop-os/cosmic-protocols", default-features = false, features = ["client"] }
|
||||||
cascade = "1.0.0"
|
libcosmic = { git = "https://github.com/pop-os/libcosmic/", branch = "master", default-features = false, features = ["wayland", "applet"] }
|
||||||
gtk4 = { git = "https://github.com/gtk-rs/gtk4-rs", features = ["v4_4"] }
|
ron = "0.8"
|
||||||
adw = { git = "https://gitlab.gnome.org/World/Rust/libadwaita-rs", package = "libadwaita"}
|
futures = "0.3"
|
||||||
libcosmic = { git = "https://github.com/pop-os/libcosmic", default-features = false }
|
futures-util = "0.3"
|
||||||
gio = { git = "https://github.com/gtk-rs/gtk-rs-core" }
|
once_cell = "1.9"
|
||||||
relm4-macros = { git = "https://github.com/Relm4/Relm4.git", branch = "next" }
|
xdg = "2.4"
|
||||||
serde_json = "1.0.78"
|
|
||||||
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"
|
pretty_env_logger = "0.4"
|
||||||
i18n-embed = { version = "0.13.4", features = ["fluent-system", "desktop-requester"] }
|
calloop = "0.10"
|
||||||
i18n-embed-fl = "0.6.4"
|
nix = "0.26"
|
||||||
rust-embed = "6.3.0"
|
anyhow = "1.0"
|
||||||
calloop = "0.10.1"
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
wayland-backend = { git = "https://github.com/Smithay/wayland-rs", version = "0.1.0-beta.9"}
|
|
||||||
wayland-client = { git = "https://github.com/Smithay/wayland-rs", version = "0.30.0-beta.9"}
|
|
||||||
nix = "0.25"
|
|
||||||
# config
|
|
||||||
anyhow = "1.0.53"
|
|
||||||
ron = "0.8.0"
|
|
||||||
serde = { version = "1.0.136", features = ["derive"] }
|
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
|
tokio = { version = "1.17.0", features = ["sync", "rt", "rt-multi-thread", "macros"] }
|
||||||
|
itertools = "*"
|
||||||
|
cosmic-panel-config = {git = "https://github.com/pop-os/cosmic-panel", no-default-features = true}
|
||||||
|
freedesktop-desktop-entry = "0.5.0"
|
||||||
|
freedesktop-icons = {git = "https://github.com/wash2/freedestkop-icons"}
|
||||||
|
i18n-embed = { version = "0.13", features = ["fluent-system", "desktop-requester"] }
|
||||||
|
i18n-embed-fl = "0.6"
|
||||||
|
rust-embed = "6.3"
|
||||||
|
|
||||||
[build-dependencies]
|
|
||||||
glib-build-tools = { git = "https://github.com/gtk-rs/gtk-rs-core" }
|
[dependencies.iced]
|
||||||
|
git = "https://github.com/pop-os/iced.git"
|
||||||
|
branch = "sctk-cosmic"
|
||||||
|
# path = "../iced"
|
||||||
|
default-features = false
|
||||||
|
features = ["image", "svg", "tokio", "wayland"]
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
fn main() {
|
|
||||||
glib_build_tools::compile_resources(
|
|
||||||
"data/resources",
|
|
||||||
"data/resources/resources.gresource.xml",
|
|
||||||
"compiled.gresource",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<schemalist>
|
|
||||||
<schema path="/com/System76/CosmicAppList/" id="com.System76.CosmicAppList">
|
|
||||||
<key name="window-width" type="i">
|
|
||||||
<default>600</default>
|
|
||||||
<summary>Default window width</summary>
|
|
||||||
<description>Default window width</description>
|
|
||||||
</key>
|
|
||||||
<key name="window-height" type="i">
|
|
||||||
<default>400</default>
|
|
||||||
<summary>Default window height</summary>
|
|
||||||
<description>Default window height</description>
|
|
||||||
</key>
|
|
||||||
<key name="is-maximized" type="b">
|
|
||||||
<default>false</default>
|
|
||||||
<summary>Default window maximized behaviour</summary>
|
|
||||||
<description></description>
|
|
||||||
</key>
|
|
||||||
</schema>
|
|
||||||
</schemalist>
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<gresources>
|
|
||||||
<gresource prefix="/com/System76/CosmicAppList/">
|
|
||||||
<!-- see https://gtk-rs.org/gtk4-rs/git/docs/gtk4/struct.Application.html#automatic-resources -->
|
|
||||||
</gresource>
|
|
||||||
</gresources>
|
|
||||||
363
applets/cosmic-app-list/src/app.rs
Normal file
363
applets/cosmic-app-list/src/app.rs
Normal file
|
|
@ -0,0 +1,363 @@
|
||||||
|
use std::ffi::OsStr;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use crate::config;
|
||||||
|
use crate::config::AppListConfig;
|
||||||
|
use crate::toplevel_subscription::toplevel_subscription;
|
||||||
|
use crate::toplevel_subscription::ToplevelRequest;
|
||||||
|
use crate::toplevel_subscription::ToplevelUpdate;
|
||||||
|
use calloop::channel::Sender;
|
||||||
|
use cctk::toplevel_info::ToplevelInfo;
|
||||||
|
use cosmic::applet::CosmicAppletHelper;
|
||||||
|
use cosmic::iced::wayland::popup::destroy_popup;
|
||||||
|
use cosmic::iced::wayland::popup::get_popup;
|
||||||
|
use cosmic::iced::wayland::SurfaceIdWrapper;
|
||||||
|
use cosmic::iced::widget::{column, row};
|
||||||
|
use cosmic::iced::{executor, window, Application, Command, Subscription};
|
||||||
|
use cosmic::iced_style::application::{self, Appearance};
|
||||||
|
use cosmic::iced_style::Color;
|
||||||
|
use cosmic::theme::Button;
|
||||||
|
use cosmic::widget::{horizontal_rule, vertical_rule};
|
||||||
|
use cosmic::{Element, Theme};
|
||||||
|
use cosmic_panel_config::PanelAnchor;
|
||||||
|
use cosmic_protocols::toplevel_info::v1::client::zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1;
|
||||||
|
use freedesktop_desktop_entry::DesktopEntry;
|
||||||
|
use iced::Alignment;
|
||||||
|
use iced::Background;
|
||||||
|
use iced::wayland::window::resize_window;
|
||||||
|
use iced::widget::container;
|
||||||
|
use iced::widget::horizontal_space;
|
||||||
|
use iced::widget::svg;
|
||||||
|
use iced::widget::Image;
|
||||||
|
use iced::Length;
|
||||||
|
use itertools::Itertools;
|
||||||
|
|
||||||
|
pub fn run() -> cosmic::iced::Result {
|
||||||
|
let helper = CosmicAppletHelper::default();
|
||||||
|
CosmicAppList::run(helper.window_settings())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
struct Toplevel {
|
||||||
|
toplevels: Vec<(ZcosmicToplevelHandleV1, ToplevelInfo)>,
|
||||||
|
app_id: String,
|
||||||
|
icon_path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
struct CosmicAppList {
|
||||||
|
theme: Theme,
|
||||||
|
popup: Option<window::Id>,
|
||||||
|
id_ctr: u32,
|
||||||
|
subscription_ctr: u32,
|
||||||
|
toplevel_list: Vec<Toplevel>,
|
||||||
|
config: AppListConfig,
|
||||||
|
toplevel_sender: Option<Sender<ToplevelRequest>>,
|
||||||
|
applet_helper: CosmicAppletHelper,
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO DnD after sctk merges DnD
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
enum Message {
|
||||||
|
Toplevel(ToplevelUpdate),
|
||||||
|
Favorite(String),
|
||||||
|
UnFavorite(String),
|
||||||
|
TogglePopup(usize),
|
||||||
|
Activate(Option<ZcosmicToplevelHandleV1>),
|
||||||
|
Quit(ZcosmicToplevelHandleV1),
|
||||||
|
Errored(String),
|
||||||
|
Ignore,
|
||||||
|
}
|
||||||
|
fn icon_for_app_ids(mut app_ids: Vec<String>) -> Vec<(String, PathBuf)> {
|
||||||
|
let mut ret = freedesktop_desktop_entry::Iter::new(freedesktop_desktop_entry::default_paths())
|
||||||
|
.filter_map(|path| {
|
||||||
|
std::fs::read_to_string(&path).ok().and_then(|input| {
|
||||||
|
DesktopEntry::decode(&path, &input).ok().and_then(|de| {
|
||||||
|
if let Some(i) = app_ids.iter().position(|s| s == de.appid) {
|
||||||
|
let id = app_ids.remove(i);
|
||||||
|
freedesktop_icons::lookup(de.icon().unwrap_or(de.appid))
|
||||||
|
.with_size(128)
|
||||||
|
.with_cache()
|
||||||
|
.find()
|
||||||
|
.map(|buf| (id, buf))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect_vec();
|
||||||
|
ret.append(
|
||||||
|
&mut app_ids
|
||||||
|
.into_iter()
|
||||||
|
.map(|id| (id, Default::default()))
|
||||||
|
.collect_vec(),
|
||||||
|
);
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Application for CosmicAppList {
|
||||||
|
type Message = Message;
|
||||||
|
type Theme = Theme;
|
||||||
|
type Executor = executor::Default;
|
||||||
|
type Flags = ();
|
||||||
|
|
||||||
|
fn new(_flags: ()) -> (Self, Command<Message>) {
|
||||||
|
let config = config::AppListConfig::load().unwrap_or_default();
|
||||||
|
(
|
||||||
|
CosmicAppList {
|
||||||
|
toplevel_list: icon_for_app_ids(config.favorites.clone())
|
||||||
|
.into_iter()
|
||||||
|
.map(|e| Toplevel {
|
||||||
|
toplevels: Default::default(),
|
||||||
|
app_id: e.0,
|
||||||
|
icon_path: e.1,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
config,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Command::none(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn title(&self) -> String {
|
||||||
|
config::APP_ID.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, message: Message) -> Command<Message> {
|
||||||
|
match message {
|
||||||
|
Message::Errored(_) => {
|
||||||
|
// TODO log errors
|
||||||
|
}
|
||||||
|
Message::TogglePopup(_) => {
|
||||||
|
if let Some(p) = self.popup.take() {
|
||||||
|
return destroy_popup(p);
|
||||||
|
} else {
|
||||||
|
self.id_ctr += 1;
|
||||||
|
let new_id = window::Id::new(self.id_ctr);
|
||||||
|
self.popup.replace(new_id);
|
||||||
|
|
||||||
|
let popup_settings = self.applet_helper.get_popup_settings(
|
||||||
|
window::Id::new(0),
|
||||||
|
new_id,
|
||||||
|
(400, 240),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
return get_popup(popup_settings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message::Favorite(id) => {
|
||||||
|
let _ = self.config.add_favorite(id);
|
||||||
|
}
|
||||||
|
Message::UnFavorite(id) => {
|
||||||
|
let _ = self.config.remove_favorite(id);
|
||||||
|
}
|
||||||
|
Message::Activate(handle) => {
|
||||||
|
if let (Some(tx), Some(handle)) = (self.toplevel_sender.as_ref(), handle) {
|
||||||
|
let _ = tx.send(ToplevelRequest::Activate(handle));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message::Quit(handle) => {
|
||||||
|
if let Some(tx) = self.toplevel_sender.as_ref() {
|
||||||
|
let _ = tx.send(ToplevelRequest::Quit(handle));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message::Toplevel(event) => {
|
||||||
|
// dbg!(&self.toplevel_list);
|
||||||
|
match event {
|
||||||
|
ToplevelUpdate::AddToplevel(handle, info) => {
|
||||||
|
if let Some(i) = self
|
||||||
|
.toplevel_list
|
||||||
|
.iter()
|
||||||
|
.position(|Toplevel { app_id, .. }| app_id == &info.app_id)
|
||||||
|
{
|
||||||
|
self.toplevel_list[i].toplevels.push((handle, info));
|
||||||
|
} else {
|
||||||
|
let (app_id, icon_name) =
|
||||||
|
icon_for_app_ids(vec![info.app_id.clone()]).remove(0);
|
||||||
|
|
||||||
|
self.toplevel_list.push(Toplevel {
|
||||||
|
toplevels: vec![(handle, info)],
|
||||||
|
app_id,
|
||||||
|
icon_path: icon_name,
|
||||||
|
});
|
||||||
|
// TODO better way of setting window size?
|
||||||
|
let pixel_size = self.applet_helper.suggested_icon_size();
|
||||||
|
let padding = 8;
|
||||||
|
let dot_size = 4;
|
||||||
|
let spacing = 4;
|
||||||
|
let length = self.toplevel_list.iter().map(|t| (pixel_size + 2 * padding).max((dot_size + spacing) * t.toplevels.len() as u16) as u32 + spacing as u32).sum();
|
||||||
|
let thickness = (pixel_size + 2 * padding + dot_size + spacing) as u32;
|
||||||
|
let (w, h) = match self.applet_helper.anchor {
|
||||||
|
PanelAnchor::Left | PanelAnchor::Right => (thickness, length),
|
||||||
|
PanelAnchor::Top | PanelAnchor::Bottom => (length, thickness),
|
||||||
|
};
|
||||||
|
return resize_window(window::Id::new(0), w, h);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ToplevelUpdate::Init(tx) => {
|
||||||
|
self.toplevel_sender.replace(tx);
|
||||||
|
}
|
||||||
|
ToplevelUpdate::Finished => {
|
||||||
|
self.subscription_ctr += 1;
|
||||||
|
}
|
||||||
|
ToplevelUpdate::RemoveToplevel(handle) => {
|
||||||
|
if let Some(i) = self.toplevel_list.iter_mut().position(
|
||||||
|
|Toplevel {
|
||||||
|
toplevels, app_id, ..
|
||||||
|
}| {
|
||||||
|
if let Some(ret) = toplevels.iter().position(|t| &t.0 == &handle) {
|
||||||
|
toplevels.remove(ret);
|
||||||
|
toplevels.is_empty() && self.config.favorites.contains(app_id)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
self.toplevel_list.remove(i);
|
||||||
|
}
|
||||||
|
// TODO better way of setting window size?
|
||||||
|
let pixel_size = self.applet_helper.suggested_icon_size();
|
||||||
|
let padding = 8;
|
||||||
|
let dot_size = 4;
|
||||||
|
let spacing = 4;
|
||||||
|
let length = self.toplevel_list.iter().map(|t| (pixel_size + 2 * padding).max((dot_size + spacing) * t.toplevels.len() as u16) as u32 + spacing as u32).sum();
|
||||||
|
let thickness = (pixel_size + 2 * padding + dot_size + spacing) as u32;
|
||||||
|
let (w, h) = match self.applet_helper.anchor {
|
||||||
|
PanelAnchor::Left | PanelAnchor::Right => (thickness, length),
|
||||||
|
PanelAnchor::Top | PanelAnchor::Bottom => (length, thickness),
|
||||||
|
};
|
||||||
|
return resize_window(window::Id::new(0), w, h);
|
||||||
|
}
|
||||||
|
ToplevelUpdate::UpdateToplevel(handle, info) => {
|
||||||
|
'toplevel_loop: for toplevel_list in &mut self.toplevel_list {
|
||||||
|
for (t_handle, t_info) in &mut toplevel_list.toplevels {
|
||||||
|
if &handle == t_handle {
|
||||||
|
*t_info = info;
|
||||||
|
break 'toplevel_loop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO better way of setting window size?
|
||||||
|
let pixel_size = self.applet_helper.suggested_icon_size();
|
||||||
|
let padding = 8;
|
||||||
|
let dot_size = 4;
|
||||||
|
let spacing = 4;
|
||||||
|
let length = self.toplevel_list.iter().map(|t| (pixel_size + 2 * padding).max((dot_size + spacing) * t.toplevels.len() as u16) as u32 + spacing as u32).sum();
|
||||||
|
let thickness = (pixel_size + 2 * padding + dot_size + spacing) as u32;
|
||||||
|
let (w, h) = match self.applet_helper.anchor {
|
||||||
|
PanelAnchor::Left | PanelAnchor::Right => (thickness, length),
|
||||||
|
PanelAnchor::Top | PanelAnchor::Bottom => (length, thickness),
|
||||||
|
};
|
||||||
|
return resize_window(window::Id::new(0), w, h);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message::Ignore => {}
|
||||||
|
}
|
||||||
|
Command::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view(&self, id: SurfaceIdWrapper) -> Element<Message> {
|
||||||
|
match id {
|
||||||
|
SurfaceIdWrapper::LayerSurface(_) => unimplemented!(),
|
||||||
|
SurfaceIdWrapper::Window(_) => {
|
||||||
|
let (favorites, running) = self.toplevel_list.iter().enumerate().fold(
|
||||||
|
(Vec::new(), Vec::new()),
|
||||||
|
|(mut favorites, mut running),
|
||||||
|
(
|
||||||
|
i,
|
||||||
|
Toplevel {
|
||||||
|
toplevels,
|
||||||
|
app_id,
|
||||||
|
icon_path,
|
||||||
|
},
|
||||||
|
)| {
|
||||||
|
let icon = if icon_path.extension() == Some(&OsStr::new("svg")) {
|
||||||
|
let handle = svg::Handle::from_path(icon_path);
|
||||||
|
svg::Svg::new(handle)
|
||||||
|
.width(Length::Units(self.applet_helper.suggested_icon_size()))
|
||||||
|
.height(Length::Units(self.applet_helper.suggested_icon_size()))
|
||||||
|
.into()
|
||||||
|
} else {
|
||||||
|
Image::new(icon_path)
|
||||||
|
.width(Length::Units(self.applet_helper.suggested_icon_size()))
|
||||||
|
.height(Length::Units(self.applet_helper.suggested_icon_size()))
|
||||||
|
.into()
|
||||||
|
};
|
||||||
|
let dot_size = (self.applet_helper.suggested_icon_size() / 8).max(2);
|
||||||
|
let dots = (0..toplevels.len())
|
||||||
|
.into_iter()
|
||||||
|
.map(|_| {
|
||||||
|
container(horizontal_space(Length::Units(0)))
|
||||||
|
.padding(dot_size)
|
||||||
|
.style(<Self::Theme as container::StyleSheet>::Style::Custom(
|
||||||
|
|theme| container::Appearance {
|
||||||
|
text_color: Some(Color::TRANSPARENT),
|
||||||
|
background: Some(Background::Color(theme.cosmic().on_bg_color().into())),
|
||||||
|
border_radius: 4.0,
|
||||||
|
border_width: 0.0,
|
||||||
|
border_color: Color::TRANSPARENT,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
.into()
|
||||||
|
})
|
||||||
|
.collect_vec();
|
||||||
|
|
||||||
|
let icon_wrapper = match &self.applet_helper.anchor {
|
||||||
|
PanelAnchor::Left => row(vec![column(dots).spacing(2).into(), icon]).align_items(iced::Alignment::Center).spacing(2).into(),
|
||||||
|
PanelAnchor::Right => row(vec![icon, column(dots).spacing(2).into()]).align_items(iced::Alignment::Center).spacing(2).into(),
|
||||||
|
PanelAnchor::Top => column(vec![row(dots).spacing(2).into(), icon]).align_items(iced::Alignment::Center).spacing(2).into(),
|
||||||
|
PanelAnchor::Bottom => column(vec![icon, row(dots).spacing(2).into()]).align_items(iced::Alignment::Center).spacing(2).into(),
|
||||||
|
};
|
||||||
|
// TODO tooltip on hover
|
||||||
|
let icon_button = cosmic::widget::button(Button::Text)
|
||||||
|
.custom(vec![icon_wrapper])
|
||||||
|
.on_press(Message::Activate(toplevels.first().map(|t| t.0.clone())))
|
||||||
|
.padding(8).into();
|
||||||
|
if self.config.favorites.contains(&app_id) {
|
||||||
|
favorites.push(icon_button)
|
||||||
|
} else {
|
||||||
|
running.push(icon_button);
|
||||||
|
}
|
||||||
|
(favorites, running)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
match &self.applet_helper.anchor {
|
||||||
|
PanelAnchor::Left | PanelAnchor::Right => {
|
||||||
|
column![column(favorites), horizontal_rule(1), column(running)].spacing(4).align_items(Alignment::Center).height(Length::Fill).width(Length::Fill).into()
|
||||||
|
}
|
||||||
|
PanelAnchor::Top | PanelAnchor::Bottom => {
|
||||||
|
row![row(favorites), vertical_rule(1), row(running)].spacing(4).align_items(Alignment::Center).height(Length::Fill).width(Length::Fill).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SurfaceIdWrapper::Popup(_) => {
|
||||||
|
todo!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn subscription(&self) -> Subscription<Message> {
|
||||||
|
Subscription::batch(vec![
|
||||||
|
toplevel_subscription(self.subscription_ctr).map(|(_, event)| Message::Toplevel(event))
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn theme(&self) -> Theme {
|
||||||
|
self.theme
|
||||||
|
}
|
||||||
|
|
||||||
|
fn close_requested(&self, _id: SurfaceIdWrapper) -> Self::Message {
|
||||||
|
Message::Ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
fn style(&self) -> <Self::Theme as application::StyleSheet>::Style {
|
||||||
|
<Self::Theme as application::StyleSheet>::Style::Custom(|theme| Appearance {
|
||||||
|
background_color: Color::from_rgba(0.0, 0.0, 0.0, 0.0),
|
||||||
|
text_color: theme.cosmic().on_bg_color().into(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,12 +1,15 @@
|
||||||
use crate::ID;
|
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use xdg::BaseDirectories;
|
use xdg::BaseDirectories;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
pub const APP_ID: &str = "com.system76.CosmicAppList";
|
||||||
|
pub const VERSION: &str = "0.1.0";
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Default)]
|
||||||
pub enum TopLevelFilter {
|
pub enum TopLevelFilter {
|
||||||
|
#[default]
|
||||||
ActiveWorkspace,
|
ActiveWorkspace,
|
||||||
ConfiguredOutput,
|
ConfiguredOutput,
|
||||||
}
|
}
|
||||||
|
|
@ -14,6 +17,7 @@ pub enum TopLevelFilter {
|
||||||
#[derive(Debug, Clone, Default, Deserialize)]
|
#[derive(Debug, Clone, Default, Deserialize)]
|
||||||
pub struct AppListConfig {
|
pub struct AppListConfig {
|
||||||
pub filter_top_levels: Option<TopLevelFilter>,
|
pub filter_top_levels: Option<TopLevelFilter>,
|
||||||
|
pub favorites: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppListConfig {
|
impl AppListConfig {
|
||||||
|
|
@ -21,7 +25,7 @@ impl AppListConfig {
|
||||||
pub fn load() -> anyhow::Result<AppListConfig> {
|
pub fn load() -> anyhow::Result<AppListConfig> {
|
||||||
let file = match BaseDirectories::new()
|
let file = match BaseDirectories::new()
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|dirs| dirs.find_config_file(format!("{ID}/config.ron")))
|
.and_then(|dirs| dirs.find_config_file(format!("{APP_ID}/config.ron")))
|
||||||
.and_then(|p| File::open(p).ok())
|
.and_then(|p| File::open(p).ok())
|
||||||
{
|
{
|
||||||
Some(path) => path,
|
Some(path) => path,
|
||||||
|
|
@ -33,4 +37,20 @@ impl AppListConfig {
|
||||||
ron::de::from_reader::<_, AppListConfig>(file)
|
ron::de::from_reader::<_, AppListConfig>(file)
|
||||||
.map_err(|err| anyhow!("Failed to parse config file: {}", err))
|
.map_err(|err| anyhow!("Failed to parse config file: {}", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn add_favorite(&mut self, id: String) -> anyhow::Result<()> {
|
||||||
|
if !self.favorites.contains(&id) {
|
||||||
|
self.favorites.push(id);
|
||||||
|
}
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_favorite(&mut self, id: String) -> anyhow::Result<()> {
|
||||||
|
self.favorites.retain(|e| e != &id);
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save() -> anyhow::Result<()> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,3 +36,12 @@ macro_rules! fl {
|
||||||
pub fn localizer() -> Box<dyn Localizer> {
|
pub fn localizer() -> Box<dyn Localizer> {
|
||||||
Box::from(DefaultLocalizer::new(&*LANGUAGE_LOADER, &Localizations))
|
Box::from(DefaultLocalizer::new(&*LANGUAGE_LOADER, &Localizations))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn localize() {
|
||||||
|
let localizer = crate::localize::localizer();
|
||||||
|
let requested_languages = i18n_embed::DesktopLanguageRequester::requested_languages();
|
||||||
|
|
||||||
|
if let Err(error) = localizer.select(&requested_languages) {
|
||||||
|
eprintln!("Error while loading language for App List {}", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,292 +1,24 @@
|
||||||
// SPDX-License-Identifier: MPL-2.0-only
|
// SPDX-License-Identifier: MPL-2.0-only
|
||||||
|
mod app;
|
||||||
use apps_window::CosmicAppListWindow;
|
|
||||||
use calloop::channel::SyncSender;
|
|
||||||
use dock_list::DockListType;
|
|
||||||
use dock_object::DockObject;
|
|
||||||
use gio::{ApplicationFlags, DesktopAppInfo};
|
|
||||||
use gtk4::gdk::Display;
|
|
||||||
use gtk4::{glib, prelude::*, CssProvider, StyleContext};
|
|
||||||
use once_cell::sync::OnceCell;
|
|
||||||
use std::collections::BTreeMap;
|
|
||||||
use utils::{AppListEvent, BoxedWindowList};
|
|
||||||
use wayland::{Toplevel, ToplevelEvent};
|
|
||||||
|
|
||||||
mod apps_container;
|
|
||||||
mod apps_window;
|
|
||||||
mod config;
|
mod config;
|
||||||
mod dock_item;
|
|
||||||
mod dock_list;
|
|
||||||
mod dock_object;
|
|
||||||
mod dock_popover;
|
|
||||||
mod localize;
|
mod localize;
|
||||||
mod utils;
|
mod toplevel_handler;
|
||||||
mod wayland;
|
mod toplevel_subscription;
|
||||||
mod wayland_source;
|
|
||||||
|
|
||||||
const ID: &str = "com.system76.CosmicAppList";
|
use log::info;
|
||||||
static TX: OnceCell<glib::Sender<AppListEvent>> = OnceCell::new();
|
|
||||||
static WAYLAND_TX: OnceCell<SyncSender<ToplevelEvent>> = OnceCell::new();
|
|
||||||
|
|
||||||
pub fn localize() {
|
use localize::localize;
|
||||||
let localizer = crate::localize::localizer();
|
|
||||||
let requested_languages = i18n_embed::DesktopLanguageRequester::requested_languages();
|
|
||||||
|
|
||||||
if let Err(error) = localizer.select(&requested_languages) {
|
use crate::config::{APP_ID, VERSION};
|
||||||
eprintln!("Error while loading language for App List {}", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_css() {
|
|
||||||
let provider = CssProvider::new();
|
|
||||||
provider.load_from_data(include_bytes!("style.css"));
|
|
||||||
|
|
||||||
StyleContext::add_provider_for_display(
|
|
||||||
&Display::default().unwrap(),
|
|
||||||
&provider,
|
|
||||||
gtk4::STYLE_PROVIDER_PRIORITY_APPLICATION,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
let _monitors = libcosmic::init();
|
|
||||||
|
|
||||||
|
fn main() -> cosmic::iced::Result {
|
||||||
// Initialize logger
|
// Initialize logger
|
||||||
pretty_env_logger::init();
|
pretty_env_logger::init();
|
||||||
glib::set_application_name("Cosmic Dock App List");
|
info!("Iced Workspaces Applet ({})", APP_ID);
|
||||||
|
info!("Version: {}", VERSION);
|
||||||
|
|
||||||
|
// Prepare i18n
|
||||||
localize();
|
localize();
|
||||||
|
|
||||||
gio::resources_register_include!("compiled.gresource").unwrap();
|
app::run()
|
||||||
let app = gtk4::Application::new(None, ApplicationFlags::default());
|
|
||||||
|
|
||||||
app.connect_activate(|app| {
|
|
||||||
load_css();
|
|
||||||
let (tx, rx) = glib::MainContext::channel(glib::Priority::default());
|
|
||||||
|
|
||||||
let window = CosmicAppListWindow::new(app);
|
|
||||||
let wayland_tx = wayland::spawn_toplevels();
|
|
||||||
|
|
||||||
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();
|
|
||||||
|
|
||||||
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_name() == 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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>() {
|
|
||||||
// clear active if it has some, they will be updated back if they still exist
|
|
||||||
let prev_active: BoxedWindowList = dock_obj.property("active");
|
|
||||||
if !prev_active.0.is_empty() {
|
|
||||||
dock_obj.set_property("active", BoxedWindowList::default().to_value());
|
|
||||||
saved_app_model.items_changed(saved_i, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
124
applets/cosmic-app-list/src/toplevel_handler.rs
Normal file
124
applets/cosmic-app-list/src/toplevel_handler.rs
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
use cctk::{
|
||||||
|
sctk::{self, event_loop::WaylandSource},
|
||||||
|
toplevel_info::{ToplevelInfoHandler, ToplevelInfoState},
|
||||||
|
wayland_client,
|
||||||
|
};
|
||||||
|
use cosmic_protocols::toplevel_info::v1::client::zcosmic_toplevel_handle_v1;
|
||||||
|
use futures::channel::mpsc::UnboundedSender;
|
||||||
|
use sctk::registry::{ProvidesRegistryState, RegistryState};
|
||||||
|
use wayland_client::{globals::registry_queue_init, Connection, QueueHandle};
|
||||||
|
|
||||||
|
use crate::toplevel_subscription::{ToplevelRequest, ToplevelUpdate};
|
||||||
|
|
||||||
|
struct AppData {
|
||||||
|
exit: bool,
|
||||||
|
tx: UnboundedSender<ToplevelUpdate>,
|
||||||
|
registry_state: RegistryState,
|
||||||
|
toplevel_info_state: ToplevelInfoState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProvidesRegistryState for AppData {
|
||||||
|
fn registry(&mut self) -> &mut RegistryState {
|
||||||
|
&mut self.registry_state
|
||||||
|
}
|
||||||
|
|
||||||
|
sctk::registry_handlers!();
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToplevelInfoHandler for AppData {
|
||||||
|
fn toplevel_info_state(&mut self) -> &mut ToplevelInfoState {
|
||||||
|
&mut self.toplevel_info_state
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_toplevel(
|
||||||
|
&mut self,
|
||||||
|
_conn: &Connection,
|
||||||
|
_qh: &QueueHandle<Self>,
|
||||||
|
toplevel: &zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1,
|
||||||
|
) {
|
||||||
|
if let Some(info) = self.toplevel_info_state.info(toplevel) {
|
||||||
|
let _ = self
|
||||||
|
.tx
|
||||||
|
.unbounded_send(ToplevelUpdate::AddToplevel(toplevel.clone(), info.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_toplevel(
|
||||||
|
&mut self,
|
||||||
|
_conn: &Connection,
|
||||||
|
_qh: &QueueHandle<Self>,
|
||||||
|
toplevel: &zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1,
|
||||||
|
) {
|
||||||
|
if let Some(info) = self.toplevel_info_state.info(toplevel) {
|
||||||
|
let _ = self.tx.unbounded_send(ToplevelUpdate::UpdateToplevel(
|
||||||
|
toplevel.clone(),
|
||||||
|
info.clone(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn toplevel_closed(
|
||||||
|
&mut self,
|
||||||
|
_conn: &Connection,
|
||||||
|
_qh: &QueueHandle<Self>,
|
||||||
|
toplevel: &zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1,
|
||||||
|
) {
|
||||||
|
let _ = self
|
||||||
|
.tx
|
||||||
|
.unbounded_send(ToplevelUpdate::RemoveToplevel(toplevel.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn toplevel_handler(
|
||||||
|
tx: UnboundedSender<ToplevelUpdate>,
|
||||||
|
rx: calloop::channel::Channel<ToplevelRequest>,
|
||||||
|
) {
|
||||||
|
let conn = Connection::connect_to_env().unwrap();
|
||||||
|
let (globals, event_queue) = registry_queue_init(&conn).unwrap();
|
||||||
|
let mut event_loop = calloop::EventLoop::<AppData>::try_new().unwrap();
|
||||||
|
let qh = event_queue.handle();
|
||||||
|
let wayland_source = WaylandSource::new(event_queue).unwrap();
|
||||||
|
let handle = event_loop.handle();
|
||||||
|
|
||||||
|
if handle
|
||||||
|
.insert_source(wayland_source, |_, q, state| q.dispatch_pending(state))
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if handle
|
||||||
|
.insert_source(rx, |event, _, state| match event {
|
||||||
|
calloop::channel::Event::Msg(req) => match req {
|
||||||
|
ToplevelRequest::Activate(_) => {} // TODO
|
||||||
|
ToplevelRequest::Quit(_) => {} // TODO
|
||||||
|
ToplevelRequest::Exit => {
|
||||||
|
state.exit = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
calloop::channel::Event::Closed => {
|
||||||
|
state.exit = true;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let registry_state = RegistryState::new(&globals);
|
||||||
|
let mut app_data = AppData {
|
||||||
|
exit: false,
|
||||||
|
tx,
|
||||||
|
toplevel_info_state: ToplevelInfoState::new(®istry_state, &qh),
|
||||||
|
registry_state,
|
||||||
|
};
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if app_data.exit {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
event_loop.dispatch(None, &mut app_data).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sctk::delegate_registry!(AppData);
|
||||||
|
cctk::delegate_toplevel_info!(AppData);
|
||||||
71
applets/cosmic-app-list/src/toplevel_subscription.rs
Normal file
71
applets/cosmic-app-list/src/toplevel_subscription.rs
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
//! # DBus interface proxy for: `org.freedesktop.UPower.KbdBacklight`
|
||||||
|
//!
|
||||||
|
//! This code was generated by `zbus-xmlgen` `2.0.1` from DBus introspection data.
|
||||||
|
//! Source: `Interface '/org/freedesktop/UPower/KbdBacklight' from service 'org.freedesktop.UPower' on system bus`.
|
||||||
|
|
||||||
|
use cctk::toplevel_info::ToplevelInfo;
|
||||||
|
use cosmic::iced;
|
||||||
|
use cosmic_protocols::toplevel_info::v1::client::zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1;
|
||||||
|
use futures::{
|
||||||
|
channel::mpsc::{unbounded, UnboundedReceiver},
|
||||||
|
StreamExt,
|
||||||
|
};
|
||||||
|
use iced::subscription;
|
||||||
|
use std::{fmt::Debug, hash::Hash};
|
||||||
|
|
||||||
|
use crate::toplevel_handler::toplevel_handler;
|
||||||
|
|
||||||
|
pub fn toplevel_subscription<I: 'static + Hash + Copy + Send + Sync + Debug>(
|
||||||
|
id: I,
|
||||||
|
) -> iced::Subscription<(I, ToplevelUpdate)> {
|
||||||
|
subscription::unfold(id, State::Ready, move |state| start_listening(id, state))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum State {
|
||||||
|
Ready,
|
||||||
|
Waiting(
|
||||||
|
UnboundedReceiver<ToplevelUpdate>,
|
||||||
|
calloop::channel::Sender<ToplevelRequest>,
|
||||||
|
),
|
||||||
|
Finished,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn start_listening<I: Copy>(id: I, state: State) -> (Option<(I, ToplevelUpdate)>, State) {
|
||||||
|
match state {
|
||||||
|
State::Ready => {
|
||||||
|
let (calloop_tx, calloop_rx) = calloop::channel::channel();
|
||||||
|
let (toplevel_tx, toplevel_rx) = unbounded();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
toplevel_handler(toplevel_tx, calloop_rx);
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
Some((id, ToplevelUpdate::Init(calloop_tx.clone()))),
|
||||||
|
State::Waiting(toplevel_rx, calloop_tx),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
State::Waiting(mut rx, tx) => match rx.next().await {
|
||||||
|
Some(u) => (Some((id, u)), State::Waiting(rx, tx)),
|
||||||
|
None => {
|
||||||
|
let _ = tx.send(ToplevelRequest::Exit);
|
||||||
|
(Some((id, ToplevelUpdate::Finished)), State::Finished)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
State::Finished => iced::futures::future::pending().await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum ToplevelUpdate {
|
||||||
|
Finished,
|
||||||
|
AddToplevel(ZcosmicToplevelHandleV1, ToplevelInfo),
|
||||||
|
UpdateToplevel(ZcosmicToplevelHandleV1, ToplevelInfo),
|
||||||
|
RemoveToplevel(ZcosmicToplevelHandleV1),
|
||||||
|
Init(calloop::channel::Sender<ToplevelRequest>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum ToplevelRequest {
|
||||||
|
Activate(ZcosmicToplevelHandleV1),
|
||||||
|
Quit(ZcosmicToplevelHandleV1),
|
||||||
|
Exit,
|
||||||
|
}
|
||||||
|
|
@ -1,597 +0,0 @@
|
||||||
use crate::config::AppListConfig;
|
|
||||||
use crate::{config::TopLevelFilter, utils::AppListEvent, wayland_source::WaylandSource, TX};
|
|
||||||
use calloop::channel::*;
|
|
||||||
use cosmic_panel_config::CosmicPanelOuput;
|
|
||||||
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 std::{env, os::unix::net::UnixStream, path::PathBuf, time::Duration};
|
|
||||||
use wayland_client::protocol::wl_seat::{self, WlSeat};
|
|
||||||
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 = std::env::var("COSMIC_PANEL_OUTPUT").ok().and_then(|size| {
|
|
||||||
match size.parse::<CosmicPanelOuput>() {
|
|
||||||
Ok(CosmicPanelOuput::Name(n)) => Some(n),
|
|
||||||
// TODO handle Active & panic if the space is still configured for All instead of being assigned a named 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, ());
|
|
||||||
|
|
||||||
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>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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, ());
|
|
||||||
state.toplevel_info = Some(ti);
|
|
||||||
}
|
|
||||||
"zcosmic_toplevel_manager_v1" => {
|
|
||||||
let tm = registry
|
|
||||||
.bind::<ZcosmicToplevelManagerV1, _, _>(name, 1, qh, ());
|
|
||||||
state.toplevel_manager = Some(tm);
|
|
||||||
}
|
|
||||||
"zcosmic_workspace_manager_v1" => {
|
|
||||||
let workspace_manager = registry
|
|
||||||
.bind::<ZcosmicWorkspaceManagerV1, _, _>(name, 1, qh, ());
|
|
||||||
state.workspace_manager = Some(workspace_manager);
|
|
||||||
}
|
|
||||||
"wl_seat" => {
|
|
||||||
registry.bind::<WlSeat, _, _>(name, 1, qh, ());
|
|
||||||
}
|
|
||||||
"wl_output" => {
|
|
||||||
registry.bind::<WlOutput, _, _>(name, 1, qh, ());
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
let removed_toplevel = state.toplevels.remove(i);
|
|
||||||
if match state.config.filter_top_levels {
|
|
||||||
Some(TopLevelFilter::ActiveWorkspace) => state
|
|
||||||
.workspace_groups
|
|
||||||
.iter()
|
|
||||||
.find(|g| {
|
|
||||||
g.workspaces
|
|
||||||
.iter()
|
|
||||||
.find(|w| {
|
|
||||||
w.states
|
|
||||||
.contains(&zcosmic_workspace_handle_v1::State::Active)
|
|
||||||
&& Some(&w.workspace_handle)
|
|
||||||
== removed_toplevel.workspace.as_ref()
|
|
||||||
})
|
|
||||||
.is_some()
|
|
||||||
})
|
|
||||||
.is_some(),
|
|
||||||
Some(TopLevelFilter::ConfiguredOutput) => {
|
|
||||||
state.expected_output == removed_toplevel.output
|
|
||||||
}
|
|
||||||
_ => true,
|
|
||||||
} {
|
|
||||||
let tx = TX.get().unwrap().clone();
|
|
||||||
let _ = tx.send(AppListEvent::Remove(removed_toplevel));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,219 +0,0 @@
|
||||||
//! 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, AsRawFd}};
|
|
||||||
|
|
||||||
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().as_raw_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 { sender_id, interface, opcode }) => {
|
|
||||||
log::error!(
|
|
||||||
"Bad message on interface \"{}\": (opcode: {}, sender_id: {:?})",
|
|
||||||
interface,
|
|
||||||
opcode,
|
|
||||||
sender_id,
|
|
||||||
);
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
7
debian/rules
vendored
7
debian/rules
vendored
|
|
@ -71,6 +71,13 @@ override_dh_auto_clean:
|
||||||
echo 'directory = "vendor"' >> .cargo/config; \
|
echo 'directory = "vendor"' >> .cargo/config; \
|
||||||
tar pcf vendor.tar vendor; \
|
tar pcf vendor.tar vendor; \
|
||||||
rm -rf vendor; \
|
rm -rf vendor; \
|
||||||
|
cd ../..; \
|
||||||
|
cd applets/cosmic-app-list/; \
|
||||||
|
mkdir -p .cargo; \
|
||||||
|
cargo vendor --sync Cargo.toml | head -n -1 > .cargo/config; \
|
||||||
|
echo 'directory = "vendor"' >> .cargo/config; \
|
||||||
|
tar pcf vendor.tar vendor; \
|
||||||
|
rm -rf vendor; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
override_dh_auto_build:
|
override_dh_auto_build:
|
||||||
|
|
|
||||||
6
justfile
6
justfile
|
|
@ -28,6 +28,9 @@ workspaces_button_id := 'com.system76.CosmicPanelWorkspacesButton'
|
||||||
|
|
||||||
build: _extract_vendor
|
build: _extract_vendor
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
pushd applets/cosmic-app-list/
|
||||||
|
cargo build {{cargo_args}}
|
||||||
|
popd
|
||||||
pushd applets/cosmic-applet-audio/
|
pushd applets/cosmic-applet-audio/
|
||||||
cargo build {{cargo_args}}
|
cargo build {{cargo_args}}
|
||||||
popd
|
popd
|
||||||
|
|
@ -63,7 +66,7 @@ install:
|
||||||
install -Dm0644 applets/cosmic-app-list/data/icons/{{app_list_id}}.Devel.svg {{iconsdir}}/{{app_list_id}}.Devel.svg
|
install -Dm0644 applets/cosmic-app-list/data/icons/{{app_list_id}}.Devel.svg {{iconsdir}}/{{app_list_id}}.Devel.svg
|
||||||
install -Dm0644 applets/cosmic-app-list/data/icons/{{app_list_id}}.svg {{iconsdir}}/{{app_list_id}}.svg
|
install -Dm0644 applets/cosmic-app-list/data/icons/{{app_list_id}}.svg {{iconsdir}}/{{app_list_id}}.svg
|
||||||
install -Dm0644 applets/cosmic-app-list/data/{{app_list_id}}.desktop {{sharedir}}/applications/{{app_list_id}}.desktop
|
install -Dm0644 applets/cosmic-app-list/data/{{app_list_id}}.desktop {{sharedir}}/applications/{{app_list_id}}.desktop
|
||||||
install -Dm0755 target/release/cosmic-app-list {{bindir}}/cosmic-app-list
|
install -Dm0755 applets/cosmic-app-list/target/release/cosmic-app-list {{bindir}}/cosmic-app-list
|
||||||
|
|
||||||
# network
|
# network
|
||||||
install -Dm0644 applets/cosmic-applet-network/data/icons/{{network_id}}.svg {{iconsdir}}/{{network_id}}.svg
|
install -Dm0644 applets/cosmic-applet-network/data/icons/{{network_id}}.svg {{iconsdir}}/{{network_id}}.svg
|
||||||
|
|
@ -128,4 +131,5 @@ _extract_vendor:
|
||||||
rm -rf applets/cosmic-applet-power/vendor; tar xf applets/cosmic-applet-power/vendor.tar --directory applets/cosmic-applet-power
|
rm -rf applets/cosmic-applet-power/vendor; tar xf applets/cosmic-applet-power/vendor.tar --directory applets/cosmic-applet-power
|
||||||
rm -rf applets/cosmic-applet-time/vendor; tar xf applets/cosmic-applet-time/vendor.tar --directory applets/cosmic-applet-time
|
rm -rf applets/cosmic-applet-time/vendor; tar xf applets/cosmic-applet-time/vendor.tar --directory applets/cosmic-applet-time
|
||||||
rm -rf applets/cosmic-applet-network/vendor; tar xf applets/cosmic-applet-network/vendor.tar --directory applets/cosmic-applet-network
|
rm -rf applets/cosmic-applet-network/vendor; tar xf applets/cosmic-applet-network/vendor.tar --directory applets/cosmic-applet-network
|
||||||
|
rm -rf applets/cosmic-app-list/vendor; tar xf applets/cosmic-app-list/vendor.tar --directory applets/cosmic-app-list
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue