From 82ac6aac1c28d3ba6f9b5dd67c2bd55ddcf05f34 Mon Sep 17 00:00:00 2001 From: Ashley Wulber <48420062+wash2@users.noreply.github.com> Date: Mon, 5 Jun 2023 23:55:37 -0400 Subject: [PATCH] Panel applets jammy (#41) --- Cargo.lock | 224 +++- Cargo.toml | 2 +- app/Cargo.toml | 6 + app/src/app.rs | 72 +- app/src/main.rs | 2 + app/src/pages/desktop/panel/applets.rs | 1472 +++++++++++++++++++++++- app/src/pages/desktop/panel/mod.rs | 22 +- app/src/pages/mod.rs | 1 + app/src/subscription/desktop_files.rs | 95 ++ app/src/subscription/mod.rs | 2 + i18n/en/cosmic_settings.ftl | 9 + 11 files changed, 1845 insertions(+), 62 deletions(-) create mode 100644 app/src/subscription/desktop_files.rs create mode 100644 app/src/subscription/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 1f8ee07..1dfc9dc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -75,9 +75,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04" +checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" dependencies = [ "memchr", ] @@ -462,10 +462,11 @@ checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" [[package]] name = "calloop" -version = "0.10.5" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a59225be45a478d772ce015d9743e49e92798ece9e34eda9a6aa2a6a7f40192" +checksum = "52e0d00eb1ea24371a97d2da6201c6747a633dc6dc1988ef503403b4c59504a8" dependencies = [ + "bitflags 1.3.2", "log", "nix 0.25.1", "slotmap", @@ -730,7 +731,7 @@ dependencies = [ [[package]] name = "cosmic-config" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic#31f7e97d5bf4860be5afd406209eed733f736f04" +source = "git+https://github.com/pop-os/libcosmic#cf2818c4a163caaa6a627e7aa6bcd40e455c7a6a" dependencies = [ "atomicwrites", "calloop", @@ -745,7 +746,7 @@ dependencies = [ [[package]] name = "cosmic-config-derive" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic#31f7e97d5bf4860be5afd406209eed733f736f04" +source = "git+https://github.com/pop-os/libcosmic#cf2818c4a163caaa6a627e7aa6bcd40e455c7a6a" dependencies = [ "quote", "syn 1.0.109", @@ -769,6 +770,7 @@ dependencies = [ name = "cosmic-settings" version = "0.1.0" dependencies = [ + "anyhow", "apply", "async-channel", "color-eyre", @@ -780,10 +782,14 @@ dependencies = [ "derive_setters", "dirs 5.0.1", "downcast-rs", + "env_logger", + "freedesktop-desktop-entry", "generator", "i18n-embed", "i18n-embed-fl", "libcosmic", + "log", + "notify", "once_cell", "regex", "rust-embed", @@ -791,6 +797,7 @@ dependencies = [ "tokio", "tracing", "tracing-subscriber", + "url", ] [[package]] @@ -867,7 +874,7 @@ dependencies = [ [[package]] name = "cosmic-theme" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic#31f7e97d5bf4860be5afd406209eed733f736f04" +source = "git+https://github.com/pop-os/libcosmic#cf2818c4a163caaa6a627e7aa6bcd40e455c7a6a" dependencies = [ "anyhow", "cosmic-config", @@ -1143,6 +1150,15 @@ dependencies = [ "dirs-sys 0.3.7", ] +[[package]] +name = "dirs" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30baa043103c9d0c2a57cf537cc2f35623889dc0d405e6c3cccfadbc81c71309" +dependencies = [ + "dirs-sys 0.3.7", +] + [[package]] name = "dirs" version = "4.0.0" @@ -1203,11 +1219,11 @@ dependencies = [ [[package]] name = "dlib" -version = "0.5.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac1b7517328c04c2aa68422fc60a41b92208182142ed04a25879c26c8f878794" +checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" dependencies = [ - "libloading 0.7.4", + "libloading 0.8.0", ] [[package]] @@ -1306,6 +1322,19 @@ dependencies = [ "syn 2.0.18", ] +[[package]] +name = "env_logger" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + [[package]] name = "errno" version = "0.3.1" @@ -1374,15 +1403,15 @@ dependencies = [ [[package]] name = "exr" -version = "1.6.3" +version = "1.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdd2162b720141a91a054640662d3edce3d50a944a50ffca5313cd951abb35b4" +checksum = "279d3efcc55e19917fff7ab3ddd6c14afb6a90881a0078465196fe2f99d08c56" dependencies = [ "bit_field", "flume", "half", "lebe", - "miniz_oxide 0.6.2", + "miniz_oxide 0.7.1", "rayon-core", "smallvec", "zune-inflate", @@ -1583,6 +1612,15 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" +[[package]] +name = "form_urlencoded" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +dependencies = [ + "percent-encoding", +] + [[package]] name = "fraction" version = "0.13.1" @@ -1593,6 +1631,19 @@ dependencies = [ "num", ] +[[package]] +name = "freedesktop-desktop-entry" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45157175a725e81f3f594382430b6b78af5f8f72db9bd51b94f0785f80fc6d29" +dependencies = [ + "dirs 3.0.2", + "gettext-rs", + "memchr", + "thiserror", + "xdg", +] + [[package]] name = "freedesktop-icons" version = "0.2.3" @@ -1788,6 +1839,26 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "gettext-rs" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e49ea8a8fad198aaa1f9655a2524b64b70eb06b2f3ff37da407566c93054f364" +dependencies = [ + "gettext-sys", + "locale_config", +] + +[[package]] +name = "gettext-sys" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c63ce2e00f56a206778276704bbe38564c8695249fdc8f354b4ef71c57c3839d" +dependencies = [ + "cc", + "temp-dir", +] + [[package]] name = "gif" version = "0.12.0" @@ -1812,9 +1883,9 @@ checksum = "518faa5064866338b013ff9b2350dc318e14cc4fcd6cb8206d7e7c9886c98815" [[package]] name = "glow" -version = "0.12.1" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e007a07a24de5ecae94160f141029e9a347282cfe25d1d58d85d845cf3130f1" +checksum = "807edf58b70c0b5b2181dd39fe1839dbdb3ba02645630dc5f753e23da307f762" dependencies = [ "js-sys", "slotmap", @@ -1973,6 +2044,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + [[package]] name = "i18n-config" version = "0.4.4" @@ -2046,7 +2123,7 @@ dependencies = [ [[package]] name = "iced" version = "0.9.0" -source = "git+https://github.com/pop-os/libcosmic#31f7e97d5bf4860be5afd406209eed733f736f04" +source = "git+https://github.com/pop-os/libcosmic#cf2818c4a163caaa6a627e7aa6bcd40e455c7a6a" dependencies = [ "iced_accessibility", "iced_core", @@ -2062,7 +2139,7 @@ dependencies = [ [[package]] name = "iced_accessibility" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic#31f7e97d5bf4860be5afd406209eed733f736f04" +source = "git+https://github.com/pop-os/libcosmic#cf2818c4a163caaa6a627e7aa6bcd40e455c7a6a" dependencies = [ "accesskit", "accesskit_unix", @@ -2071,7 +2148,7 @@ dependencies = [ [[package]] name = "iced_core" version = "0.9.0" -source = "git+https://github.com/pop-os/libcosmic#31f7e97d5bf4860be5afd406209eed733f736f04" +source = "git+https://github.com/pop-os/libcosmic#cf2818c4a163caaa6a627e7aa6bcd40e455c7a6a" dependencies = [ "bitflags 1.3.2", "iced_accessibility", @@ -2086,7 +2163,7 @@ dependencies = [ [[package]] name = "iced_futures" version = "0.6.0" -source = "git+https://github.com/pop-os/libcosmic#31f7e97d5bf4860be5afd406209eed733f736f04" +source = "git+https://github.com/pop-os/libcosmic#cf2818c4a163caaa6a627e7aa6bcd40e455c7a6a" dependencies = [ "futures", "iced_core", @@ -2099,7 +2176,7 @@ dependencies = [ [[package]] name = "iced_graphics" version = "0.8.0" -source = "git+https://github.com/pop-os/libcosmic#31f7e97d5bf4860be5afd406209eed733f736f04" +source = "git+https://github.com/pop-os/libcosmic#cf2818c4a163caaa6a627e7aa6bcd40e455c7a6a" dependencies = [ "bitflags 1.3.2", "bytemuck", @@ -2116,7 +2193,7 @@ dependencies = [ [[package]] name = "iced_renderer" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic#31f7e97d5bf4860be5afd406209eed733f736f04" +source = "git+https://github.com/pop-os/libcosmic#cf2818c4a163caaa6a627e7aa6bcd40e455c7a6a" dependencies = [ "iced_graphics", "iced_tiny_skia", @@ -2128,7 +2205,7 @@ dependencies = [ [[package]] name = "iced_runtime" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic#31f7e97d5bf4860be5afd406209eed733f736f04" +source = "git+https://github.com/pop-os/libcosmic#cf2818c4a163caaa6a627e7aa6bcd40e455c7a6a" dependencies = [ "iced_accessibility", "iced_core", @@ -2140,7 +2217,7 @@ dependencies = [ [[package]] name = "iced_sctk" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic#31f7e97d5bf4860be5afd406209eed733f736f04" +source = "git+https://github.com/pop-os/libcosmic#cf2818c4a163caaa6a627e7aa6bcd40e455c7a6a" dependencies = [ "enum-repr", "float-cmp", @@ -2161,7 +2238,7 @@ dependencies = [ [[package]] name = "iced_style" version = "0.8.0" -source = "git+https://github.com/pop-os/libcosmic#31f7e97d5bf4860be5afd406209eed733f736f04" +source = "git+https://github.com/pop-os/libcosmic#cf2818c4a163caaa6a627e7aa6bcd40e455c7a6a" dependencies = [ "iced_core", "once_cell", @@ -2171,7 +2248,7 @@ dependencies = [ [[package]] name = "iced_tiny_skia" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic#31f7e97d5bf4860be5afd406209eed733f736f04" +source = "git+https://github.com/pop-os/libcosmic#cf2818c4a163caaa6a627e7aa6bcd40e455c7a6a" dependencies = [ "bytemuck", "cosmic-text", @@ -2189,7 +2266,7 @@ dependencies = [ [[package]] name = "iced_wgpu" version = "0.10.0" -source = "git+https://github.com/pop-os/libcosmic#31f7e97d5bf4860be5afd406209eed733f736f04" +source = "git+https://github.com/pop-os/libcosmic#cf2818c4a163caaa6a627e7aa6bcd40e455c7a6a" dependencies = [ "bitflags 1.3.2", "bytemuck", @@ -2211,7 +2288,7 @@ dependencies = [ [[package]] name = "iced_widget" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic#31f7e97d5bf4860be5afd406209eed733f736f04" +source = "git+https://github.com/pop-os/libcosmic#cf2818c4a163caaa6a627e7aa6bcd40e455c7a6a" dependencies = [ "iced_renderer", "iced_runtime", @@ -2226,7 +2303,7 @@ dependencies = [ [[package]] name = "iced_winit" version = "0.9.1" -source = "git+https://github.com/pop-os/libcosmic#31f7e97d5bf4860be5afd406209eed733f736f04" +source = "git+https://github.com/pop-os/libcosmic#cf2818c4a163caaa6a627e7aa6bcd40e455c7a6a" dependencies = [ "iced_graphics", "iced_runtime", @@ -2312,6 +2389,16 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "image" version = "0.24.6" @@ -2415,6 +2502,18 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "is-terminal" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" +dependencies = [ + "hermit-abi 0.3.1", + "io-lifetimes", + "rustix", + "windows-sys 0.48.0", +] + [[package]] name = "itertools" version = "0.10.5" @@ -2511,14 +2610,14 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" [[package]] name = "libc" -version = "0.2.144" +version = "0.2.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" +checksum = "fc86cde3ff845662b8f4ef6cb50ea0e20c524eb3d29ae048287e06a1b3fa6a81" [[package]] name = "libcosmic" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic#31f7e97d5bf4860be5afd406209eed733f736f04" +source = "git+https://github.com/pop-os/libcosmic#cf2818c4a163caaa6a627e7aa6bcd40e455c7a6a" dependencies = [ "apply", "cosmic-config", @@ -2528,6 +2627,8 @@ dependencies = [ "freedesktop-icons", "iced", "iced_core", + "iced_futures", + "iced_renderer", "iced_runtime", "iced_sctk", "iced_style", @@ -3075,9 +3176,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.17.2" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9670a07f94779e00908f3e686eab508878ebb390ba6e604d3a284c00e8d0487b" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] name = "option-ext" @@ -3245,9 +3346,9 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" [[package]] name = "phf" @@ -3572,9 +3673,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.8.3" +version = "1.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81ca098a9821bd52d6b24fd8b10bd081f47d39c22778cafaa75a2857a62c6390" +checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f" dependencies = [ "aho-corasick", "memchr", @@ -4226,6 +4327,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "temp-dir" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af547b166dd1ea4b472165569fc456cfb6818116f854690b0ff205e636523dab" + [[package]] name = "tempfile" version = "3.5.0" @@ -4359,6 +4466,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.28.2" @@ -4593,6 +4715,15 @@ dependencies = [ "regex", ] +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + [[package]] name = "unicode-script" version = "0.5.5" @@ -4623,6 +4754,17 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" +[[package]] +name = "url" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + [[package]] name = "usvg" version = "0.32.0" @@ -5062,9 +5204,9 @@ dependencies = [ [[package]] name = "wgpu-hal" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41af2ea7d87bd41ad0a37146252d5f7c26490209f47f544b2ee3b3ff34c7732e" +checksum = "74851c2c8e5d97652e74c241d41b0656b31c924a45dcdecde83975717362cfa4" dependencies = [ "android_system_properties", "arrayvec 0.7.2", @@ -5534,9 +5676,9 @@ dependencies = [ [[package]] name = "xml-rs" -version = "0.8.13" +version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d8f380ae16a37b30e6a2cf67040608071384b1450c189e61bea3ff57cde922d" +checksum = "52839dc911083a8ef63efa4d039d1f58b5e409f923e44c80828f206f66e5541c" [[package]] name = "xmlparser" diff --git a/Cargo.toml b/Cargo.toml index 90112c5..833ad99 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,4 +19,4 @@ branch = "settings_jammy" [workspace.dependencies.cosmic-panel-config] git = "https://github.com/pop-os/cosmic-panel" -branch = "settings_jammy" \ No newline at end of file +branch = "settings_jammy" diff --git a/app/Cargo.toml b/app/Cargo.toml index 09ecc34..bc415d2 100644 --- a/app/Cargo.toml +++ b/app/Cargo.toml @@ -28,6 +28,12 @@ downcast-rs = "1.2.0" cosmic-panel-config = { workspace = true } tracing = "0.1.37" tracing-subscriber = { version = "0.3.17", features = ["env-filter"]} +log = "0.4" +env_logger = "0.10" +url = "2.3.1" +freedesktop-desktop-entry = "0.5.0" +notify = "6.0.0" +anyhow = "1.0" [dependencies.i18n-embed] version = "0.13.9" diff --git a/app/src/app.rs b/app/src/app.rs index c978d91..e6caf5e 100644 --- a/app/src/app.rs +++ b/app/src/app.rs @@ -3,9 +3,11 @@ use apply::Apply; +use cosmic_panel_config::CosmicPanelConfig; use cosmic_settings_page::{self as page, section}; use cosmic::{ + cosmic_config::config_subscription, iced::{ self, event::wayland::{self, WindowEvent, WindowState}, @@ -30,13 +32,20 @@ use cosmic::{ use crate::{ config::Config, pages::{ - desktop::{self, panel}, + desktop::{ + self, + panel::{ + self, + applets::{self, APPLET_DND_ICON_ID}, + }, + }, sound, system, time, }, + subscription::desktop_files, widget::{page_title, parent_page_button, search_header, sub_page_button}, }; -use std::process; +use std::{borrow::Cow, process}; #[allow(clippy::struct_excessive_bools)] #[allow(clippy::module_name_repetitions)] @@ -77,6 +86,8 @@ pub enum Message { ToggleNavBarCondensed, WindowResize(u32, u32), WindowState(WindowState), + PanelConfig(CosmicPanelConfig), + DesktopInfo, } impl Application for SettingsApp { @@ -114,6 +125,8 @@ impl Application for SettingsApp { // app.insert_page::(); let desktop_id = app.insert_page::().id(); + // app.insert_page::(); + // app.insert_page::(); // app.insert_page::(); @@ -174,9 +187,22 @@ impl Application for SettingsApp { Subscription::batch(vec![ window_break, keyboard_nav::subscription().map(Message::KeyboardNav), + desktop_files(0).map(|_| Message::DesktopInfo), + config_subscription(0, "com.system76.CosmicPanel.panel".into(), 1).map( + |(_, e)| match e { + Ok(config) => Message::PanelConfig(config), + Err((errors, config)) => { + for error in errors { + log::error!("Error loading panel config: {:?}", error); + } + Message::PanelConfig(config) + } + }, + ), ]) } + #[allow(clippy::too_many_lines)] fn update(&mut self, message: Message) -> iced::Command { let mut ret = Command::none(); match message { @@ -257,17 +283,53 @@ impl Application for SettingsApp { page.update(message); } } + crate::pages::Message::Applet(message) => { + if let Some(page) = self.pages.page_mut::() { + ret = page.update(message); + } + } }, Message::WindowState(state) => { - dbg!(&state); self.sharp_corners = matches!(state, WindowState::Activated); } + Message::PanelConfig(config) if config.name.to_lowercase().contains("panel") => { + if let Some(page) = self.pages.page_mut::() { + page.update(panel::Message::PanelConfig(config.clone())); + } + if let Some(page) = self.pages.page_mut::() { + _ = page.update(applets::Message::PanelConfig(config)); + } + } + Message::PanelConfig(_) => {} // ignore other config changes for now, + Message::DesktopInfo => { + if let Some(page) = self.pages.page_mut::() { + // collect the potential applets + ret = page.update(applets::Message::Applets( + freedesktop_desktop_entry::Iter::new( + freedesktop_desktop_entry::default_paths(), + ) + .filter_map(|p| applets::Applet::try_from(Cow::from(p)).ok()) + .collect(), + )); + } + } } ret } #[allow(clippy::too_many_lines)] - fn view(&self, _id: window::Id) -> Element { + fn view(&self, id: window::Id) -> Element { + if let Some(Some(page)) = + (id == APPLET_DND_ICON_ID).then(|| self.pages.page::()) + { + return page.dnd_icon(); + } + if let Some(Some(page)) = + (id == applets::ADD_APPLET_DIALOGUE_ID).then(|| self.pages.page::()) + { + return page.add_applet_view(); + } + let (nav_bar_message, nav_bar_toggled) = if self.is_condensed { ( Message::ToggleNavBarCondensed, @@ -335,7 +397,7 @@ impl Application for SettingsApp { ); } - let content = container(row(widgets)) + let content = container(row(widgets).spacing(8)) .padding([0, 8, 8, 8]) .width(Length::Fill) .height(Length::Fill) diff --git a/app/src/main.rs b/app/src/main.rs index 6abe37b..ca57046 100644 --- a/app/src/main.rs +++ b/app/src/main.rs @@ -16,6 +16,8 @@ pub mod pages; pub mod theme; pub mod widget; +pub mod subscription; + use cosmic::{ iced::{wayland::actions::window::SctkWindowSettings, Application, Limits}, iced_sctk::settings::InitialSurface, diff --git a/app/src/pages/desktop/panel/applets.rs b/app/src/pages/desktop/panel/applets.rs index e663e8a..e81b797 100644 --- a/app/src/pages/desktop/panel/applets.rs +++ b/app/src/pages/desktop/panel/applets.rs @@ -1,8 +1,103 @@ -use cosmic_settings_page::{self as page, section, Section}; -use slotmap::SlotMap; +use std::{ + borrow::{Borrow, Cow}, + fmt::Debug, + mem, + path::{Path, PathBuf}, + str::FromStr, + sync::Arc, +}; -#[derive(Default)] -pub struct Page; +use apply::Apply; +use cosmic::{ + cosmic_config::{self, Config, CosmicConfigEntry}, + iced::{ + alignment::{Horizontal, Vertical}, + event::{ + self, + wayland::{self}, + PlatformSpecific, + }, + mouse, overlay, touch, + wayland::actions::{ + data_device::{ActionInner, DataFromMimeType, DndIcon}, + window::SctkWindowSettings, + }, + wayland::data_device::action as data_device_action, + window, Alignment, Color, Length, Point, Rectangle, Size, + }, + iced_runtime::{command::platform_specific, core::id::Id, Command}, + iced_sctk::commands, + iced_style::{ + button::StyleSheet as ButtonStyleSheet, container::StyleSheet as ContainerStyleSheet, + }, + iced_widget::{ + column, container, + core::{ + layout, renderer, + widget::{tree, Operation, OperationOutputWrapper, Tree}, + Clipboard, Shell, Widget, + }, + graphics::image::image_rs::EncodableLayout, + row, scrollable, text, text_input, + text_input::{Icon, Side}, + Column, + }, + sctk::reexports::client::protocol::wl_data_device_manager::DndAction, + theme, + widget::{button, header_bar, icon, list_column}, + Element, +}; + +use crate::{app, pages}; +use cosmic_panel_config::CosmicPanelConfig; +use cosmic_settings_page::{self as page, section, Section}; +use freedesktop_desktop_entry::DesktopEntry; +use slotmap::SlotMap; +use tracing::error; + +const MIME_TYPE: &str = "text/uri-list"; + +pub type OnDndCommand<'a, Message> = Box< + dyn Fn( + Box platform_specific::wayland::data_device::ActionInner>, + ) -> Message + + 'a, +>; + +const SPACING: f32 = 8.0; + +// radius is 8.0 +const DRAG_START_DISTANCE_SQUARED: f32 = 64.0; + +pub const APPLET_DND_ICON_ID: window::Id = window::Id(1000); +pub const ADD_APPLET_DIALOGUE_ID: window::Id = window::Id(1001); + +pub struct Page { + available_entries: Vec>, + config_helper: Option, + current_config: Option, + reorder_widget_state: ReorderWidgetState, + search: String, +} + +impl Default for Page { + fn default() -> Self { + let config_helper = cosmic_config::Config::new("com.system76.CosmicPanel.panel", 1).ok(); + let current_config = config_helper.as_ref().and_then(|config_helper| { + // TODO error handling... + let panel_config = CosmicPanelConfig::get_entry(config_helper).ok()?; + // If the config is not present, it will be created with the default values and the name will not match + (panel_config.name == "panel").then_some(panel_config) + }); + Self { + available_entries: Vec::new(), + config_helper, + current_config, + reorder_widget_state: ReorderWidgetState::default(), + search: String::new(), + } + } +} impl page::Page for Page { #[allow(clippy::too_many_lines)] @@ -10,13 +105,1378 @@ impl page::Page for Page { &self, sections: &mut SlotMap>, ) -> Option { - Some(vec![sections.insert(Section::default())]) + Some(vec![sections.insert(lists())]) } fn info(&self) -> page::Info { page::Info::new("panel_applets", "preferences-pop-desktop-dock-symbolic") - .title(fl!("applets")) + // .title(fl!("applets")) } } impl page::AutoBind for Page {} + +#[derive(Clone)] +pub enum Message { + RemoveStart(String), + RemoveCenter(String), + RemoveEnd(String), + DetailStart(String), + DetailCenter(String), + DetailEnd(String), + ReorderStart(Vec>), + ReorderCenter(Vec>), + ReorderEnd(Vec>), + Applets(Vec>), + PanelConfig(CosmicPanelConfig), + StartDnd(ReorderWidgetState), + DnDCommand(Arc ActionInner>>), + Search(String), + AddApplet(Applet<'static>), + AddAppletDialogue, + CloseAppletDialogue, + DragAppletDialogue, + Save, + Cancel, + Ignore, +} + +impl Debug for Message { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Message::ReorderStart(_) => write!(f, "ReorderStart"), + Message::ReorderCenter(_) => write!(f, "ReorderCenter"), + Message::ReorderEnd(_) => write!(f, "ReorderEnd"), + Message::Applets(_) => write!(f, "Applets"), + Message::PanelConfig(_) => write!(f, "PanelConfig"), + Message::StartDnd(_) => write!(f, "StartDnd"), + Message::DnDCommand(_) => write!(f, "DnDCommand"), + Message::Ignore => write!(f, "Ignore"), + Message::Save => write!(f, "ApplyReorder"), + Message::RemoveStart(_) => write!(f, "RemoveStart"), + Message::RemoveCenter(_) => write!(f, "RemoveCenter"), + Message::RemoveEnd(_) => write!(f, "RemoveEnd"), + Message::DetailStart(_) => write!(f, "DetailStart"), + Message::DetailCenter(_) => write!(f, "DetailCenter"), + Message::DetailEnd(_) => write!(f, "DetailEnd"), + Message::Cancel => write!(f, "Cancel"), + Message::Search(_) => write!(f, "Search"), + Message::AddApplet(_) => write!(f, "AddApplet"), + Message::AddAppletDialogue => write!(f, "AddAppletDialogue"), + Message::CloseAppletDialogue => write!(f, "CloseAppletDialogue"), + Message::DragAppletDialogue => write!(f, "DragAppletDialogue"), + } + } +} + +impl Page { + pub fn save(&self) { + let Some(config) = self.current_config.as_ref() else { + error!("No panel config. Failed to save applets."); + return; + }; + let Some(helper) = self.config_helper.as_ref() else { + error!("No panel config helper. Failed to save applets."); + return; + }; + if let Err(e) = config.write_entry(helper) { + error!("Failed to save applets: {:?}", e); + } + } + + #[must_use] + pub fn dnd_icon(&self) -> Element { + Element::from(AppletReorderList::dnd_icon(&self.reorder_widget_state)) + } + + #[must_use] + #[allow(clippy::too_many_lines)] + pub fn add_applet_view(&self) -> Element { + let mut list_column = list_column(); + let mut has_some = false; + for info in self + .available_entries + .iter() + .filter(|a| a.matches(&self.search)) + { + if let Some(config) = self.current_config.as_ref() { + if let Some(center) = config.plugins_center.as_ref() { + if center.iter().any(|a| a.as_str() == info.id.as_ref()) { + continue; + } + } + + if let Some(wings) = config.plugins_wings.as_ref() { + if wings + .0 + .iter() + .chain(wings.1.iter()) + .any(|a| a.as_str() == info.id.as_ref()) + { + continue; + } + } + } + has_some = true; + list_column = list_column.add( + row![ + icon(info.icon.to_owned(), 32).style(theme::Svg::Symbolic), + column![ + text(info.name.to_owned()), + text(info.description.to_owned()).size(10) + ] + .spacing(4.0) + .width(Length::Fill), + cosmic::iced::widget::button(text(fl!("add"))) + .style(theme::Button::Custom { + active: Box::new(|theme| { + let mut style = theme.active(&theme::Button::Text); + style.text_color = theme.cosmic().accent_color().into(); + style + }), + hover: Box::new(|theme| { + let mut style = theme.hovered(&theme::Button::Text); + style.text_color = theme.cosmic().accent_color().into(); + style + }) + }) + .padding(8.0) + .on_press(app::Message::PageMessage(pages::Message::Applet( + Message::AddApplet(info.clone()) + ))), + ] + .padding([0, 32, 0, 32]) + .spacing(12) + .align_items(Alignment::Center), + ); + } + if !has_some { + list_column = list_column.add( + text(fl!("no-applets-found")) + .width(Length::Fill) + .horizontal_alignment(Horizontal::Center), + ); + } + column![ + header_bar() + .title(fl!("add-applet")) + .on_close(app::Message::PageMessage(pages::Message::Applet( + Message::CloseAppletDialogue + ))) + .on_drag(app::Message::PageMessage(pages::Message::Applet( + Message::DragAppletDialogue + ))), + container( + scrollable( + column![ + text(fl!("add-applet")).size(24).width(Length::Fill), + text_input(&fl!("search-applets"), &self.search) + .style(theme::TextInput::Search) + .padding([8, 24]) + .icon(Icon { + font: cosmic::iced::Font::default(), + code_point: '🔍', + size: Some(12.0), + spacing: 12.0, + side: Side::Left, + }) + .on_input(|s| { + app::Message::PageMessage(pages::Message::Applet(Message::Search( + s, + ))) + }) + .on_paste(|s| { + app::Message::PageMessage(pages::Message::Applet(Message::Search( + s, + ))) + }) + .width(Length::Fixed(312.0)), + list_column + ] + .padding([0, 64, 32, 64]) + .align_items(Alignment::Center) + .spacing(8.0) + ) + .width(Length::Fill) + .height(Length::Fill) + ) + .style(theme::Container::Background) + .width(Length::Fill) + .height(Length::Fill) + ] + .into() + } + + #[allow(clippy::too_many_lines)] + pub fn update(&mut self, message: Message) -> Command { + match message { + Message::PanelConfig(c) => { + self.current_config = Some(c); + } + + Message::ReorderStart(start_list) => { + let Some(config) = self.current_config.as_mut() else { + return Command::none(); + }; + let Some((list, _)) = config.plugins_wings.as_mut() else { + config.plugins_wings = Some((start_list.into_iter().map(|a: Applet| a.id.into()).collect(), Vec::new())); + return Command::none(); + }; + *list = start_list.into_iter().map(|a| a.id.into()).collect(); + } + Message::ReorderCenter(center_list) => { + let Some(config) = self.current_config.as_mut() else { + return Command::none(); + }; + let Some(list) = config.plugins_center.as_mut() else { + config.plugins_center = Some(center_list.into_iter().map(|a: Applet| a.id.into()).collect()); + return Command::none(); + }; + *list = center_list.into_iter().map(|a| a.id.into()).collect(); + } + Message::ReorderEnd(end_list) => { + let Some(config) = self.current_config.as_mut() else { + return Command::none(); + }; + let Some((_, list)) = config.plugins_wings.as_mut() else { + config.plugins_wings = Some((Vec::new(), end_list.into_iter().map(|a: Applet| a.id.into()).collect())); + return Command::none(); + }; + *list = end_list.into_iter().map(|a| a.id.into()).collect(); + } + Message::Applets(applets) => { + self.available_entries = applets; + } + Message::StartDnd(state) => { + self.reorder_widget_state = state; + return Command::none(); + } + Message::DnDCommand(action) => { + return data_device_action(action()); + } + Message::Ignore => {} + Message::Save => { + self.reorder_widget_state = ReorderWidgetState::default(); + self.save(); + } + Message::RemoveStart(to_remove) => { + let Some(config) = self.current_config.as_mut() else { + return Command::none(); + }; + let Some((list, _)) = config.plugins_wings.as_mut() else { + return Command::none(); + }; + list.retain(|id| id != &to_remove); + self.save(); + } + Message::RemoveCenter(to_remove) => { + let Some(config) = self.current_config.as_mut() else { + return Command::none(); + }; + let Some(list) = config.plugins_center.as_mut() else { + return Command::none(); + }; + list.retain(|id| id != &to_remove); + self.save(); + } + Message::RemoveEnd(to_remove) => { + let Some(config) = self.current_config.as_mut() else { + return Command::none(); + }; + let Some((_, list)) = config.plugins_wings.as_mut() else { + return Command::none(); + }; + list.retain(|id| id != &to_remove); + self.save(); + } + Message::DetailStart(_) => { + // TODO ask design team + } + Message::DetailCenter(_) => { + // TODO ask design team + } + Message::DetailEnd(_) => { + // TODO ask design team + } + Message::Cancel => { + self.reorder_widget_state = ReorderWidgetState::default(); + let current_config = self.config_helper.as_ref().and_then(|config_helper| { + // TODO error handling... + let panel_config = CosmicPanelConfig::get_entry(config_helper).ok()?; + // If the config is not present, it will be created with the default values and the name will not match + (panel_config.name == "panel").then_some(panel_config) + }); + self.current_config = current_config; + } + Message::Search(text) => { + self.search = text; + } + Message::AddApplet(applet) => { + // TODO ask design team + let Some(config) = self.current_config.as_mut() else { + return Command::none(); + }; + let list = if let Some((list, _)) = config.plugins_wings.as_mut() { + list + } else { + config.plugins_wings = Some((Vec::new(), Vec::new())); + &mut config.plugins_wings.as_mut().unwrap().0 + }; + + list.push(applet.id.to_string()); + self.save(); + return commands::window::close_window(ADD_APPLET_DIALOGUE_ID); + } + Message::AddAppletDialogue => { + let window_settings = SctkWindowSettings { + window_id: ADD_APPLET_DIALOGUE_ID, + app_id: Some("com.system76.CosmicSettings".to_string()), + title: Some(fl!("add-applet")), + parent: None, + autosize: false, + size_limits: layout::Limits::NONE + .min_width(300.0) + .max_width(800.0) + .min_height(200.0) + .max_height(1080.0), + size: (512, 420), + resizable: None, + client_decorations: true, + transparent: true, + }; + return commands::window::get_window(window_settings); + } + Message::CloseAppletDialogue => { + return commands::window::close_window(ADD_APPLET_DIALOGUE_ID); + } + Message::DragAppletDialogue => { + return commands::window::start_drag_window(ADD_APPLET_DIALOGUE_ID); + } + }; + Command::none() + } +} + +#[allow(clippy::too_many_lines)] +pub fn lists() -> Section { + Section::default().view::(|_binder, page, _section| { + let Some(config) = page.current_config.as_ref() else { + return Element::from( + text(fl!("unknown")) + ); + }; + column![ + column![ + row![ + text(fl!("applets")).width(Length::Fill).size(24), + cosmic::iced::widget::button(text(fl!("add-applet"))) + .style(theme::Button::Secondary) + .padding(8.0) + .on_press(Message::AddAppletDialogue) + ], + text(fl!("start-segment")), + AppletReorderList::new( + config + .plugins_wings + .as_ref() + .map(|list| list + .0 + .iter() + .filter_map(|id| page + .available_entries + .iter() + .find(|e| e.id.as_ref() == id.as_str()) + .map(Applet::borrowed)) + .collect()) + .unwrap_or_default(), + Some((window::Id(0), APPLET_DND_ICON_ID)), + Message::StartDnd, + |a| Message::DnDCommand(Arc::new(a)), + Message::RemoveStart, + Message::DetailStart, + Message::ReorderStart, + Message::Save, + Message::Cancel, + page.reorder_widget_state.dragged_applet() + ) + ] + .spacing(8.0), + column![ + text(fl!("center-segment")), + AppletReorderList::new( + config + .plugins_center + .as_ref() + .map(|list| list + .iter() + .filter_map(|id| page + .available_entries + .iter() + .find(|e| e.id.as_ref() == id.as_str()) + .map(Applet::borrowed)) + .collect()) + .unwrap_or_default(), + Some((window::Id(0), APPLET_DND_ICON_ID)), + Message::StartDnd, + |a| Message::DnDCommand(Arc::new(a)), + Message::RemoveCenter, + Message::DetailCenter, + Message::ReorderCenter, + Message::Save, + Message::Cancel, + page.reorder_widget_state.dragged_applet() + ) + ] + .spacing(8.0), + column![ + text(fl!("end-segment")), + AppletReorderList::new( + config + .plugins_wings + .as_ref() + .map(|list| list + .1 + .iter() + .filter_map(|id| page + .available_entries + .iter() + .find(|e| e.id.as_ref() == id.as_str()) + .map(Applet::borrowed)) + .collect()) + .unwrap_or_default(), + Some((window::Id(0), APPLET_DND_ICON_ID)), + Message::StartDnd, + |a| Message::DnDCommand(Arc::new(a)), + Message::RemoveEnd, + Message::DetailEnd, + Message::ReorderEnd, + Message::Save, + Message::Cancel, + page.reorder_widget_state.dragged_applet() + ) + ] + .spacing(8.0), + ] + .padding([0, 16, 0, 16]) + .spacing(12.0) + .apply(Element::from) + .map(pages::Message::Applet) + }) +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Applet<'a> { + pub id: Cow<'a, str>, + pub name: Cow<'a, str>, + pub description: Cow<'a, str>, + pub icon: Cow<'a, str>, + pub path: Cow<'a, Path>, +} + +impl Applet<'_> { + pub fn matches(&self, query: &str) -> bool { + self.name.contains(query) || self.description.contains(query) || self.id.contains(query) + } +} + +impl<'a> TryFrom> for Applet<'static> { + type Error = anyhow::Error; + + fn try_from(path: Cow<'a, Path>) -> Result { + let content = std::fs::read_to_string(path.as_ref())?; + let entry = DesktopEntry::decode(path.as_ref(), &content)?; + if entry.desktop_entry("X-CosmicApplet").is_none() { + anyhow::bail!("Not an applet"); + } + + Ok(Self { + id: Cow::from(entry.id().to_string()), + name: Cow::from(entry.name(None).unwrap_or_default().to_string()), + description: Cow::from(entry.comment(None).unwrap_or_default().to_string()), + icon: Cow::from(entry.icon().unwrap_or_default().to_string()), + path: Cow::from(path.into_owned()), + }) + } +} + +impl Applet<'static> { + fn borrowed(&self) -> Applet<'_> { + Applet { + id: Cow::from(self.id.as_ref()), + name: Cow::from(self.name.as_ref()), + description: Cow::from(self.description.as_ref()), + icon: Cow::from(self.icon.as_ref()), + path: Cow::from(self.path.as_ref()), + } + } +} + +impl<'a> Applet<'a> { + fn into_owned(self) -> Applet<'static> { + Applet { + id: Cow::from(self.id.into_owned()), + name: Cow::from(self.name.into_owned()), + description: Cow::from(self.description.into_owned()), + icon: Cow::from(self.icon.into_owned()), + path: Cow::from(self.path.into_owned()), + } + } +} + +// TODO A11y / keyboard support + +pub struct AppletReorderList<'a, Message> { + id: Id, + info: Vec>, + on_create_dnd_source: Box Message + 'a>, + on_dnd_command_produced: OnDndCommand<'a, Message>, + on_reorder: Box>) -> Message + 'a>, + on_finish: Option, + on_cancel: Option, + surface_ids: Option<(window::Id, window::Id)>, + inner: Element<'a, Message>, +} + +impl<'a, Message: 'static + Clone> AppletReorderList<'a, Message> { + #[allow(clippy::too_many_arguments)] + #[must_use] + /// new applet list which can be reordered and dragged + pub fn new( + info: Vec>, + surface_ids: Option<(window::Id, window::Id)>, + on_create_dnd_source: impl Fn(ReorderWidgetState) -> Message + 'a, + on_dnd_command_produced: impl Fn( + Box platform_specific::wayland::data_device::ActionInner>, + ) -> Message + + 'a, + on_remove: impl Fn(String) -> Message + 'a, + on_details: impl Fn(String) -> Message + 'a, + on_reorder: impl Fn(Vec>) -> Message + 'a, + on_apply_reorder: Message, + on_cancel: Message, + active_dnd: Option>, // state: Option<&State>, + ) -> Self { + // let dragged_path = state.and_then(|state| state.dragged_applet()); + let applet_buttons = info + .clone() + .into_iter() + .map(|info| { + let id_clone = info.id.to_string(); + let is_dragged = active_dnd.as_ref().map_or(false, |dnd| dnd.id == info.id); + container( + row![ + icon("open-menu-symbolic", 16).style(theme::Svg::Symbolic), + icon(info.icon, 32).style(theme::Svg::Symbolic), + column![text(info.name), text(info.description).size(10)] + .spacing(4.0) + .width(Length::Fill), + button(theme::Button::Text) + .icon(theme::Svg::Symbolic, "edit-delete-symbolic", 16) + .on_press(on_remove(id_clone.clone())), + button(theme::Button::Text) + .icon(theme::Svg::Symbolic, "open-menu-symbolic", 16) + .on_press(on_details(id_clone)), + ] + .spacing(12) + .align_items(Alignment::Center), + ) + .width(Length::Fill) + .padding(8) + .style(theme::Container::Custom(Box::new(move |theme| { + let mut style = theme.appearance(&theme::Container::Primary); + style.border_radius = 8.0.into(); + if is_dragged { + style.border_color = theme.cosmic().accent_color().into(); + style.border_width = 2.0; + } + style + }))) + .into() + }) + .collect::>(); + + Self { + id: Id::unique(), + info, + on_create_dnd_source: Box::new(on_create_dnd_source), + on_dnd_command_produced: Box::new(on_dnd_command_produced), + on_reorder: Box::new(on_reorder), + on_finish: Some(on_apply_reorder), + on_cancel: Some(on_cancel), + surface_ids, + inner: if active_dnd.is_some() && applet_buttons.is_empty() { + container( + text(fl!("drop-here")) + .width(Length::Fill) + .height(Length::Fill) + .vertical_alignment(Vertical::Center) + .horizontal_alignment(Horizontal::Center), + ) + .width(Length::Fill) + .height(Length::Fixed(48.0)) + .padding(8) + .style(theme::Container::Custom(Box::new(move |theme| { + let mut style = theme.appearance(&theme::Container::Primary); + style.border_radius = 8.0.into(); + style.border_color = theme.cosmic().bg_divider().into(); + style.border_width = 2.0; + style.background = Color::TRANSPARENT.into(); + style + }))) + .into() + } else { + Column::with_children(applet_buttons) + .spacing(SPACING) + .into() + }, + } + } + + #[must_use] + /// mark this as a dnd icon + pub fn dnd_icon(state: &'a ReorderWidgetState) -> Self { + Self { + id: Id::unique(), + info: Vec::new(), + on_create_dnd_source: Box::new(|_| unimplemented!()), + on_dnd_command_produced: Box::new(|_| unimplemented!()), + on_reorder: Box::new(|_| unimplemented!()), + on_finish: None, + surface_ids: None, + inner: if let Some(info) = state.dragged_applet() { + container( + row![ + icon("open-menu-symbolic", 16).style(theme::Svg::Symbolic), + icon(info.icon.into_owned(), 32).style(theme::Svg::Symbolic), + column![text(info.name), text(info.description).size(10)] + .spacing(4.0) + .width(Length::Fill), + button(theme::Button::Text).icon( + theme::Svg::Symbolic, + "edit-delete-symbolic", + 16 + ), + button(theme::Button::Text).icon( + theme::Svg::Symbolic, + "open-menu-symbolic", + 16 + ), + ] + .spacing(12) + .align_items(Alignment::Center), + ) + .width(Length::Fixed(state.layout.map_or(400.0, |l| l.width))) + .padding(8) + .style(theme::Container::Custom(Box::new(move |theme| { + let mut style = theme.appearance(&theme::Container::Primary); + style.border_radius = 8.0.into(); + style + }))) + .into() + } else { + text("unknown").into() + }, + on_cancel: None, + } + } + + #[must_use] + /// reorders the list of applets given: + /// - the bounds of the list + /// - the current mouse position during a drag + /// - the applet being offered to this list + fn get_reordered( + &self, + layout: &layout::Layout, + pos: Point, + offered_applet: Applet<'a>, + ) -> Vec> { + let mut reordered: Vec<_> = self.info.clone(); + + if !layout.bounds().contains(pos) { + // applets shouldn't be in two lists at once + reordered.retain(|a| a != &offered_applet); + + return reordered; + } + + // special case + if reordered.is_empty() { + reordered.push(offered_applet); + return reordered; + } + + // special case + if reordered.len() == 1 && reordered[0] == offered_applet { + return reordered; + } + + let height = (layout.bounds().height - SPACING * (self.info.len() - 1) as f32) + / self.info.len() as f32; + + let mut found = false; + let mut y = layout.bounds().y; + + for i in 0..=reordered.len() { + if i == 0 || i == reordered.len() { + y += height / 2.0; + } else { + y += height + SPACING; + } + if pos.y <= y { + reordered.insert(i, offered_applet.clone()); + let mut index = 0; + reordered.retain(|a| { + let ret = a != &offered_applet || index == i; + index += 1; + ret + }); + + found = true; + break; + } + } + if !found { + reordered.retain(|a| a != &offered_applet); + reordered.push(offered_applet); + } + + reordered + } +} + +impl<'a, Message: 'static> Widget for AppletReorderList<'a, Message> +where + Message: Clone, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::() + } + + fn state(&self) -> tree::State { + tree::State::new(ReorderWidgetState::new()) + } + + fn children(&self) -> Vec { + vec![Tree::new(&self.inner)] + } + + fn diff(&mut self, tree: &mut Tree) { + tree.diff_children(std::slice::from_mut(&mut self.inner)); + } + + fn width(&self) -> Length { + Length::Fill + } + + fn height(&self) -> Length { + Length::Shrink + } + + fn layout(&self, renderer: &cosmic::Renderer, limits: &layout::Limits) -> layout::Node { + let inner_layout = self.inner.as_widget().layout(renderer, limits); + layout::Node::with_children(inner_layout.size(), vec![inner_layout]) + } + + fn operate( + &self, + tree: &mut Tree, + layout: layout::Layout<'_>, + renderer: &cosmic::Renderer, + operation: &mut dyn Operation>, + ) { + self.inner.as_widget().operate( + &mut tree.children[0], + layout.children().next().unwrap(), + renderer, + operation, + ); + } + + #[allow(clippy::too_many_lines)] + fn on_event( + &mut self, + tree: &mut Tree, + event: event::Event, + layout: layout::Layout<'_>, + cursor_position: Point, + renderer: &cosmic::Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + let mut ret = match self.inner.as_widget_mut().on_event( + &mut tree.children[0], + event.clone(), + layout.children().next().unwrap(), + cursor_position, + renderer, + clipboard, + shell, + ) { + event::Status::Captured => return event::Status::Captured, + event::Status::Ignored => event::Status::Ignored, + }; + + let height = (layout.bounds().height - SPACING * (self.info.len() - 1) as f32) + / self.info.len() as f32; + let state = tree.state.downcast_mut::(); + + state.dragging_state = match mem::take(&mut state.dragging_state) { + DraggingState::None => { + // if no dragging state, listen for press events + match &event { + event::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | event::Event::Touch(touch::Event::FingerPressed { .. }) + if layout.bounds().contains(cursor_position) => + { + ret = event::Status::Captured; + + DraggingState::Pressed(cursor_position) + } + _ => DraggingState::None, + } + } + DraggingState::Dragging(applet) => match &event { + event::Event::PlatformSpecific(PlatformSpecific::Wayland( + wayland::Event::DataSource(wayland::DataSourceEvent::DndFinished), + )) => { + ret = event::Status::Captured; + DraggingState::None + } + event::Event::PlatformSpecific(PlatformSpecific::Wayland( + wayland::Event::DataSource(wayland::DataSourceEvent::Cancelled), + )) => { + ret = event::Status::Captured; + if let Some(on_cancel) = self.on_cancel.clone() { + shell.publish(on_cancel); + } + DraggingState::None + } + _ => DraggingState::Dragging(applet), + }, + DraggingState::Pressed(start) => { + // if dragging state is pressed, listen for motion events or release events + match &event { + event::Event::Mouse(mouse::Event::CursorMoved { .. }) + | event::Event::Touch(touch::Event::FingerMoved { .. }) => { + let d_y = cursor_position.y - start.y; + let d_x = cursor_position.x - start.x; + let distance_squared = d_y * d_y + d_x * d_x; + + if distance_squared > DRAG_START_DISTANCE_SQUARED { + if let Some((_, applet)) = + self.info.iter().enumerate().find(|(i, _)| { + start.y + < layout.bounds().y + (*i + 1) as f32 * (height + SPACING) + }) + { + let (window_id, icon_id) = self.surface_ids.unwrap(); + state.dragging_state = + DraggingState::Dragging(applet.clone().into_owned()); + + // TODO emit a dnd command + state.layout = Some(layout.bounds().size()); + let state_clone = state.clone(); + shell.publish((self.on_create_dnd_source.as_ref())( + state_clone.clone(), + )); + + let p = applet.path.to_path_buf(); + shell.publish((self.on_dnd_command_produced.as_ref())(Box::new(move || { + platform_specific::wayland::data_device::ActionInner::StartDnd { + mime_types: vec![MIME_TYPE.to_string()], + actions: DndAction::Move, + origin_id: window_id, + icon_id: Some(DndIcon::Widget( + icon_id, + Box::new(state_clone.clone()), + )), + data: Box::new(AppletString(p.clone())), + } + }))); + ret = event::Status::Captured; + DraggingState::Dragging(applet.clone().into_owned()) + } else { + DraggingState::Pressed(start) + } + } else { + DraggingState::Pressed(start) + } + } + event::Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | event::Event::Touch( + touch::Event::FingerLifted { .. } | touch::Event::FingerLost { .. }, + ) => { + ret = event::Status::Captured; + DraggingState::None + } + _ => DraggingState::Pressed(start), + } + } + }; + state.dnd_offer = match mem::take(&mut state.dnd_offer) { + DndOfferState::None => match &event { + event::Event::PlatformSpecific(PlatformSpecific::Wayland( + wayland::Event::DndOffer(wayland::DndOfferEvent::SourceActions(actions)), + )) => DndOfferState::OutsideWidget(Vec::new(), *actions, None), + event::Event::PlatformSpecific(PlatformSpecific::Wayland( + wayland::Event::DndOffer(wayland::DndOfferEvent::Enter { x, y, mime_types }), + )) => { + if mime_types.iter().any(|m| m.as_str() == MIME_TYPE) { + let point = Point::new(*x as f32, *y as f32); + + if layout.bounds().contains(point) { + shell.publish((self.on_dnd_command_produced.as_ref())(Box::new( + move || { + platform_specific::wayland::data_device::ActionInner::SetActions { + preferred: DndAction::Move, + accepted: DndAction::Move, + } + }, + ))); + shell.publish((self.on_dnd_command_produced.as_ref())(Box::new( + move || { + platform_specific::wayland::data_device::ActionInner::Accept( + Some(MIME_TYPE.to_string()), + ) + }, + ))); + let data = match &state.dragging_state { + DraggingState::Dragging(a) => Some(a.clone()), + + _ => { + shell.publish((self.on_dnd_command_produced.as_ref())(Box::new( + move || { + platform_specific::wayland::data_device::ActionInner::RequestDndData( + MIME_TYPE.to_string(), + ) + }, + ))); + None + } + }; + DndOfferState::HandlingOffer( + mime_types.clone(), + DndAction::empty(), + data, + ) + } else { + let data = match &state.dragging_state { + DraggingState::Dragging(data) => { + let filtered: Vec<_> = self + .info + .clone() + .into_iter() + .filter(|a| a != data) + .collect(); + if filtered != self.info { + shell.publish((self.on_reorder.as_ref())( + filtered + .into_iter() + .map(pages::desktop::panel::applets::Applet::into_owned) + .collect(), + )); + } + Some(data.clone()) + } + _ => None, + }; + DndOfferState::OutsideWidget( + mime_types.clone(), + DndAction::empty(), + data, + ) + } + } else { + DndOfferState::None + } + } + _ => DndOfferState::None, + }, + DndOfferState::OutsideWidget(mime_types, action, data) => match &event { + event::Event::PlatformSpecific(PlatformSpecific::Wayland( + wayland::Event::DndOffer(wayland::DndOfferEvent::SourceActions(actions)), + )) => DndOfferState::OutsideWidget(mime_types, *actions, data), + event::Event::PlatformSpecific(PlatformSpecific::Wayland( + wayland::Event::DndOffer(wayland::DndOfferEvent::Motion { x, y }), + )) => { + let point = Point::new(*x as f32, *y as f32); + + if layout.bounds().contains(point) { + shell.publish((self.on_dnd_command_produced.as_ref())(Box::new( + move || { + platform_specific::wayland::data_device::ActionInner::SetActions { + preferred: DndAction::Move, + accepted: DndAction::Move, + } + }, + ))); + shell.publish((self.on_dnd_command_produced.as_ref())(Box::new( + move || { + platform_specific::wayland::data_device::ActionInner::Accept(Some( + MIME_TYPE.to_string(), + )) + }, + ))); + shell.publish((self.on_dnd_command_produced.as_ref())(Box::new( + move || { + platform_specific::wayland::data_device::ActionInner::SetActions { + preferred: action.intersection(DndAction::Move), + accepted: action + .intersection(DndAction::Move.union(DndAction::Copy)), + } + }, + ))); + // TODO maybe keep track of data and request here if we don't have it + // also maybe just refactor DND Targets to allow easier handling... + + if data.is_none() { + shell.publish((self.on_dnd_command_produced.as_ref())(Box::new( + move || { + platform_specific::wayland::data_device::ActionInner::RequestDndData( + MIME_TYPE.to_string(), + ) + }, + ))); + } + if let Some(applet) = data.clone() { + let reordered_list: Vec<_> = self.get_reordered( + &layout, + Point { + x: *x as f32, + y: *y as f32, + }, + applet, + ); + if reordered_list != self.info { + shell.publish((self.on_reorder.as_ref())( + reordered_list.into_iter().map(Applet::into_owned).collect(), + )); + } + } + + DndOfferState::HandlingOffer(mime_types, DndAction::empty(), data) + } else { + DndOfferState::OutsideWidget(mime_types, DndAction::empty(), data) + } + } + event::Event::PlatformSpecific(PlatformSpecific::Wayland( + wayland::Event::DndOffer(wayland::DndOfferEvent::DndData { + data: new_data, + mime_type, + }), + )) => { + if mime_type.as_str() == MIME_TYPE { + let data = std::str::from_utf8(new_data.as_bytes()) + .ok() + .and_then(|s| url::Url::from_str(s).ok()) + .and_then(|url| url.to_file_path().ok()) + .and_then(|p| Applet::try_from(Cow::from(p)).ok()); + DndOfferState::OutsideWidget(mime_types, action, data) + } else { + DndOfferState::OutsideWidget(mime_types, action, data) + } + } + event::Event::PlatformSpecific(PlatformSpecific::Wayland( + wayland::Event::DndOffer( + wayland::DndOfferEvent::DropPerformed | wayland::DndOfferEvent::Leave, + ), + )) => DndOfferState::None, + _ => DndOfferState::OutsideWidget(mime_types, action, data), + }, + DndOfferState::HandlingOffer(mime_types, action, data) => match &event { + event::Event::PlatformSpecific(PlatformSpecific::Wayland( + wayland::Event::DndOffer(wayland::DndOfferEvent::Motion { x, y }), + )) => { + let point = Point::new(*x as f32, *y as f32); + if layout.bounds().contains(point) { + shell.publish((self.on_dnd_command_produced.as_ref())(Box::new( + move || { + platform_specific::wayland::data_device::ActionInner::SetActions { + preferred: DndAction::Move, + accepted: DndAction::Move, + } + }, + ))); + if let Some(data) = data.clone() { + let reordered_list = self.get_reordered( + &layout, + Point { + x: *x as f32, + y: *y as f32, + }, + data, + ); + if reordered_list != self.info { + shell.publish((self.on_reorder.as_ref())( + reordered_list + .into_iter() + .map(pages::desktop::panel::applets::Applet::into_owned) + .collect(), + )); + } + } + DndOfferState::HandlingOffer(mime_types, DndAction::empty(), data) + } else { + if let Some(applet) = data.clone() { + let reordered_list: Vec<_> = self.get_reordered( + &layout, + Point { + x: *x as f32, + y: *y as f32, + }, + applet, + ); + if reordered_list != self.info { + shell.publish((self.on_reorder.as_ref())( + reordered_list.into_iter().map(Applet::into_owned).collect(), + )); + } + } + shell.publish((self.on_dnd_command_produced.as_ref())(Box::new( + move || { + platform_specific::wayland::data_device::ActionInner::Accept(None) + }, + ))); + DndOfferState::OutsideWidget(mime_types, DndAction::empty(), data) + } + } + event::Event::PlatformSpecific(PlatformSpecific::Wayland( + wayland::Event::DndOffer(wayland::DndOfferEvent::Leave), + )) => DndOfferState::None, + event::Event::PlatformSpecific(PlatformSpecific::Wayland( + wayland::Event::DndOffer(wayland::DndOfferEvent::SourceActions(actions)), + )) => { + shell.publish((self.on_dnd_command_produced.as_ref())(Box::new( + move || platform_specific::wayland::data_device::ActionInner::SetActions { + preferred: DndAction::Move, + accepted: DndAction::Move, + }, + ))); + DndOfferState::HandlingOffer(mime_types, *actions, data) + } + event::Event::PlatformSpecific(PlatformSpecific::Wayland( + wayland::Event::DndOffer(wayland::DndOfferEvent::DndData { + data: new_data, + mime_type, + }), + )) => { + if mime_type.as_str() == MIME_TYPE { + let data = std::str::from_utf8(new_data.as_bytes()) + .ok() + .and_then(|s| url::Url::from_str(s).ok()) + .and_then(|url| url.to_file_path().ok()) + .and_then(|p| Applet::try_from(Cow::from(p)).ok()); + if let Some(data) = data.borrow() { + let filtered: Vec<_> = self + .info + .clone() + .into_iter() + .filter(|a| a != data) + .collect(); + if filtered != self.info { + shell.publish((self.on_reorder.as_ref())( + filtered + .into_iter() + .map(pages::desktop::panel::applets::Applet::into_owned) + .collect(), + )); + } + } + + DndOfferState::HandlingOffer(mime_types, action, data) + } else { + DndOfferState::HandlingOffer(mime_types, action, data) + } + } + event::Event::PlatformSpecific(PlatformSpecific::Wayland( + wayland::Event::DndOffer(wayland::DndOfferEvent::DropPerformed), + )) => { + shell.publish((self.on_dnd_command_produced.as_ref())(Box::new( + move || platform_specific::wayland::data_device::ActionInner::SetActions { + preferred: DndAction::Move, + accepted: DndAction::Move, + }, + ))); + shell.publish((self.on_dnd_command_produced.as_ref())(Box::new( + move || { + platform_specific::wayland::data_device::ActionInner::Accept(Some( + MIME_TYPE.to_string(), + )) + }, + ))); + shell.publish((self.on_dnd_command_produced.as_ref())(Box::new( + move || { + platform_specific::wayland::data_device::ActionInner::RequestDndData( + MIME_TYPE.to_string(), + ) + }, + ))); + DndOfferState::Dropped + } + _ => DndOfferState::HandlingOffer(mime_types, action, data), + }, + DndOfferState::Dropped => match &event { + event::Event::PlatformSpecific(PlatformSpecific::Wayland( + wayland::Event::DndOffer(wayland::DndOfferEvent::DndData { .. }), + )) => { + if let Some(on_finish) = self.on_finish.clone() { + shell.publish(on_finish); + } + shell.publish((self.on_dnd_command_produced.as_ref())(Box::new( + move || platform_specific::wayland::data_device::ActionInner::DndFinished, + ))); + + DndOfferState::None + } + event::Event::PlatformSpecific(PlatformSpecific::Wayland( + wayland::Event::DndOffer(wayland::DndOfferEvent::Leave), + )) => { + // already applied the offer, so we can just finish + if let Some(on_cancel) = self.on_cancel.clone() { + shell.publish(on_cancel); + } + + DndOfferState::None + } + _ => DndOfferState::Dropped, + }, + }; + + ret + } + + fn draw( + &self, + state: &Tree, + renderer: &mut cosmic::Renderer, + theme: &cosmic::Theme, + style: &renderer::Style, + layout: layout::Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) { + self.inner.as_widget().draw( + &state.children[0], + renderer, + theme, + style, + layout.children().next().unwrap(), + cursor_position, + viewport, + ); + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: layout::Layout<'_>, + renderer: &cosmic::Renderer, + ) -> Option> { + self.inner.as_widget_mut().overlay( + &mut tree.children[0], + layout.children().next().unwrap(), + renderer, + ) + } + + fn mouse_interaction( + &self, + state: &Tree, + layout: layout::Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + renderer: &cosmic::Renderer, + ) -> mouse::Interaction { + match self.inner.as_widget().mouse_interaction( + &state.children[0], + layout.children().next().unwrap(), + cursor_position, + viewport, + renderer, + ) { + mouse::Interaction::Idle => { + let state = state.state.downcast_ref::(); + if matches!(state.dragging_state, DraggingState::Dragging(_)) { + mouse::Interaction::Grabbing + } else if layout.bounds().contains(cursor_position) { + mouse::Interaction::Grab + } else { + mouse::Interaction::default() + } + } + interaction => interaction, + } + } +} + +/// A string which can be sent to the clipboard or drag-and-dropped. +#[derive(Debug, Clone)] +pub struct AppletString(PathBuf); + +impl DataFromMimeType for AppletString { + fn from_mime_type(&self, mime_type: &str) -> Option> { + if mime_type == MIME_TYPE { + let data = Some( + url::Url::from_file_path(self.0.clone()) + .ok()? + .to_string() + .as_bytes() + .to_vec(), + ); + data + } else { + None + } + } +} + +#[derive(Debug, Default, Clone)] +pub enum DraggingState { + #[default] + /// No ongoing drag or press + None, + /// A draggable item was being pressed at the recorded point + Pressed(Point), + /// An item is being dragged + Dragging(Applet<'static>), +} + +#[derive(Debug, Default, Clone)] +pub(crate) enum DndOfferState { + #[default] + None, + OutsideWidget(Vec, DndAction, Option>), + HandlingOffer(Vec, DndAction, Option>), + Dropped, +} + +#[derive(Debug, Default, Clone)] +pub struct ReorderWidgetState { + dragging_state: DraggingState, + dnd_offer: DndOfferState, + layout: Option, +} + +impl ReorderWidgetState { + pub(crate) fn new() -> ReorderWidgetState { + ReorderWidgetState::default() + } + + pub(crate) fn dragged_applet(&self) -> Option> { + match &self.dragging_state { + DraggingState::Dragging(applet) => Some(applet.borrowed()), + _ => None, + } + } +} + +impl<'a, Message: 'static + Clone> From> for Element<'a, Message> { + fn from(applet_reorder_list: AppletReorderList<'a, Message>) -> Self { + Element::new(applet_reorder_list) + } +} +#[derive(Debug, Clone)] +pub enum State { + DndIcon(ReorderWidgetState), +} diff --git a/app/src/pages/desktop/panel/mod.rs b/app/src/pages/desktop/panel/mod.rs index 63cd180..8e433bd 100644 --- a/app/src/pages/desktop/panel/mod.rs +++ b/app/src/pages/desktop/panel/mod.rs @@ -17,7 +17,7 @@ use cosmic_settings_page::{self as page, section, Section}; use slotmap::SlotMap; use std::{borrow::Cow, collections::HashMap}; -mod applets; +pub mod applets; pub struct Page { config_helper: Option, @@ -383,6 +383,7 @@ pub enum Message { Applets, OutputAdded(String, WlOutput), OutputRemoved(WlOutput), + PanelConfig(CosmicPanelConfig), } impl Page { @@ -390,7 +391,7 @@ impl Page { match message { Message::AutoHidePanel(enabled) => { let helper = self.config_helper.as_ref().unwrap(); - let panel_config = self.panel_config.as_mut().unwrap(); + let mut panel_config = self.panel_config.as_mut().unwrap(); panel_config.autohide = enabled.then_some(AutoHide { wait_time: 1000, @@ -402,7 +403,7 @@ impl Page { } Message::PanelAnchor(anchor) => { let helper = self.config_helper.as_ref().unwrap(); - let panel_config = self.panel_config.as_mut().unwrap(); + let mut panel_config = self.panel_config.as_mut().unwrap(); panel_config.anchor = anchor; @@ -410,7 +411,7 @@ impl Page { } Message::Output(name) => { let helper = self.config_helper.as_ref().unwrap(); - let panel_config = self.panel_config.as_mut().unwrap(); + let mut panel_config = self.panel_config.as_mut().unwrap(); panel_config.output = match name { s if s == fl!("all") => CosmicPanelOuput::All, @@ -421,7 +422,7 @@ impl Page { } Message::AnchorGap(enabled) => { let helper = self.config_helper.as_ref().unwrap(); - let panel_config = self.panel_config.as_mut().unwrap(); + let mut panel_config = self.panel_config.as_mut().unwrap(); panel_config.anchor_gap = enabled; @@ -429,7 +430,7 @@ impl Page { } Message::PanelSize(size) => { let helper = self.config_helper.as_ref().unwrap(); - let panel_config = self.panel_config.as_mut().unwrap(); + let mut panel_config = self.panel_config.as_mut().unwrap(); panel_config.size = size; @@ -437,7 +438,7 @@ impl Page { } Message::Appearance(a) => { let helper = self.config_helper.as_ref().unwrap(); - let panel_config = self.panel_config.as_mut().unwrap(); + let mut panel_config = self.panel_config.as_mut().unwrap(); panel_config.background = a.into(); @@ -445,7 +446,7 @@ impl Page { } Message::ExtendToEdge(enabled) => { let helper = self.config_helper.as_ref().unwrap(); - let panel_config = self.panel_config.as_mut().unwrap(); + let mut panel_config = self.panel_config.as_mut().unwrap(); panel_config.expand_to_edges = enabled; @@ -453,7 +454,7 @@ impl Page { } Message::Opacity(opacity) => { let helper = self.config_helper.as_ref().unwrap(); - let panel_config = self.panel_config.as_mut().unwrap(); + let mut panel_config = self.panel_config.as_mut().unwrap(); panel_config.opacity = opacity; @@ -467,6 +468,9 @@ impl Page { Message::OutputRemoved(output) => { self.outputs.remove(&output.id()); } + Message::PanelConfig(c) => { + self.panel_config = Some(c); + } } } } diff --git a/app/src/pages/mod.rs b/app/src/pages/mod.rs index 6cdaf7e..019986e 100644 --- a/app/src/pages/mod.rs +++ b/app/src/pages/mod.rs @@ -16,6 +16,7 @@ pub enum Message { Desktop(desktop::Message), Panel(desktop::panel::Message), DesktopWallpaper(desktop::wallpaper::Message), + Applet(desktop::panel::applets::Message), External { id: String, message: Vec }, Page(Entity), } diff --git a/app/src/subscription/desktop_files.rs b/app/src/subscription/desktop_files.rs new file mode 100644 index 0000000..d994aa2 --- /dev/null +++ b/app/src/subscription/desktop_files.rs @@ -0,0 +1,95 @@ +use cosmic::iced::subscription; +use notify::{Config, Event, RecommendedWatcher, RecursiveMode, Watcher}; +use std::fmt::Debug; +use std::hash::Hash; +use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver}; + +#[derive(Debug)] +pub enum State { + Ready, + Waiting { + watcher: RecommendedWatcher, + rx: UnboundedReceiver>, + }, + Finished, +} + +#[derive(Debug, Clone, Copy)] +pub enum DesktopFileEvent { + Changed, +} + +pub fn desktop_files( + id: I, +) -> cosmic::iced::Subscription<(I, DesktopFileEvent)> { + subscription::unfold(id, State::Ready, move |mut state| async move { + loop { + let (event, new_state) = start_watching(id, state).await; + state = new_state; + if let Some(event) = event { + return (event, state); + } + } + }) +} + +async fn start_watching(id: I, state: State) -> (Option<(I, DesktopFileEvent)>, State) { + match state { + State::Ready => { + let paths = freedesktop_desktop_entry::default_paths(); + // TODO log errors + if let Ok((mut watcher, rx)) = async_watcher() { + for path in paths { + let _ = watcher.watch(path.as_ref(), RecursiveMode::Recursive); + } + ( + Some((id, DesktopFileEvent::Changed)), + State::Waiting { watcher, rx }, + ) + } else { + (None, State::Finished) + } + } + State::Waiting { watcher, rx } => { + if let Some(rx) = async_watch(rx).await { + ( + Some((id, DesktopFileEvent::Changed)), + State::Waiting { watcher, rx }, + ) + } else { + (None, State::Finished) + } + } + State::Finished => cosmic::iced::futures::future::pending().await, + } +} + +fn async_watcher() -> notify::Result<(RecommendedWatcher, UnboundedReceiver>)> +{ + let (tx, rx) = unbounded_channel(); + + // Automatically select the best implementation for your platform. + // You can also access each implementation directly e.g. INotifyWatcher. + let watcher = RecommendedWatcher::new( + move |res| { + _ = tx.send(res); + }, + Config::default(), + )?; + + Ok((watcher, rx)) +} + +async fn async_watch( + mut rx: UnboundedReceiver>, +) -> Option>> { + // TODO log errors + if let Some(res) = rx.recv().await { + match res { + Ok(_) => return Some(rx), + Err(_) => return None, + } + } + + None +} diff --git a/app/src/subscription/mod.rs b/app/src/subscription/mod.rs new file mode 100644 index 0000000..004dc76 --- /dev/null +++ b/app/src/subscription/mod.rs @@ -0,0 +1,2 @@ +mod desktop_files; +pub use desktop_files::*; diff --git a/i18n/en/cosmic_settings.ftl b/i18n/en/cosmic_settings.ftl index 099abb8..5645029 100644 --- a/i18n/en/cosmic_settings.ftl +++ b/i18n/en/cosmic_settings.ftl @@ -83,9 +83,18 @@ panel-missing = Panel Configuration is Missing .fix = Reset to default applets = Applets +start-segment = Start Segment +center-segment = Center Segment +end-segment = End Segment +add = Add +add-applet = Add Applet +search-applets = Search applets... +no-applets-found = No applets found... all = All +drop-here = Drop applets here + ## Desktop: Wallpaper wallpaper = Wallpaper