From 2a939e5a11b5c73dae95a9a0f3a6ba3893a5077e Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Mon, 17 Mar 2025 13:51:56 -0700 Subject: [PATCH] status-area: Handle changes to icon properties It seems status icons, at least some, don't send property change notifications. So we can't rely on that, and have to disable caching. And handle the `NewIcon` signal defined in https://www.freedesktop.org/wiki/Specifications/StatusNotifierItem/StatusNotifierItem I'm not sure whether or not there's a *good* reason it works this way, but regardless I see `nm-applet` and `ibus` update their icons as they should after these changes. --- .../src/components/status_menu.rs | 38 ++++++++- .../src/subscriptions/status_notifier_item.rs | 83 +++++++++++-------- 2 files changed, 83 insertions(+), 38 deletions(-) diff --git a/cosmic-applet-status-area/src/components/status_menu.rs b/cosmic-applet-status-area/src/components/status_menu.rs index 9f582126..0f41c149 100644 --- a/cosmic-applet-status-area/src/components/status_menu.rs +++ b/cosmic-applet-status-area/src/components/status_menu.rs @@ -7,11 +7,12 @@ use cosmic::{ widget::icon, }; -use crate::subscriptions::status_notifier_item::{Layout, StatusNotifierItem}; +use crate::subscriptions::status_notifier_item::{IconUpdate, Layout, StatusNotifierItem}; #[derive(Clone, Debug)] pub enum Msg { Layout(Result), + Icon(IconUpdate), Click(i32, bool), } @@ -19,6 +20,9 @@ pub struct State { item: StatusNotifierItem, layout: Option, expanded: Option, + icon_name: String, + // TODO handle icon with multiple sizes? + icon_pixmap: Option, } impl State { @@ -28,6 +32,8 @@ impl State { item, layout: None, expanded: None, + icon_name: String::new(), + icon_pixmap: None, }, iced::Task::none(), ) @@ -44,6 +50,27 @@ impl State { } iced::Task::none() } + Msg::Icon(update) => { + match update { + IconUpdate::Name(name) => { + self.icon_name = name; + } + IconUpdate::Pixmap(icons) => { + self.icon_pixmap = icons + .into_iter() + .max_by_key(|i| (i.width, i.height)) + .map(|mut i| { + // Convert ARGB to RGBA + for pixel in i.bytes.chunks_exact_mut(4) { + pixel.rotate_left(1); + } + icon::from_raster_pixels(i.width as u32, i.height as u32, i.bytes) + }); + } + } + + iced::Task::none() + } Msg::Click(id, is_submenu) => { let menu_proxy = self.item.menu_proxy().clone(); tokio::spawn(async move { @@ -68,11 +95,11 @@ impl State { } pub fn icon_name(&self) -> &str { - self.item.icon_name() + &self.icon_name } pub fn icon_pixmap(&self) -> Option<&icon::Handle> { - self.item.icon_pixmap() + self.icon_pixmap.as_ref() } pub fn popup_view(&self) -> cosmic::Element { @@ -84,7 +111,10 @@ impl State { } pub fn subscription(&self) -> iced::Subscription { - self.item.layout_subscription().map(Msg::Layout) + iced::Subscription::batch([ + self.item.layout_subscription().map(Msg::Layout), + self.item.icon_subscription().map(Msg::Icon), + ]) } pub fn opened(&self) { diff --git a/cosmic-applet-status-area/src/subscriptions/status_notifier_item.rs b/cosmic-applet-status-area/src/subscriptions/status_notifier_item.rs index 43593fe1..79686baf 100644 --- a/cosmic-applet-status-area/src/subscriptions/status_notifier_item.rs +++ b/cosmic-applet-status-area/src/subscriptions/status_notifier_item.rs @@ -11,18 +11,21 @@ use zbus::zvariant::{self, OwnedValue}; #[derive(Clone, Debug)] pub struct StatusNotifierItem { name: String, - icon_name: String, - // TODO Handle icon with multiple sizes? - icon_pixmap: Option, - _item_proxy: StatusNotifierItemProxy<'static>, + item_proxy: StatusNotifierItemProxy<'static>, menu_proxy: DBusMenuProxy<'static>, } #[derive(Clone, Debug, zvariant::Value)] pub struct Icon { - width: i32, - height: i32, - bytes: Vec, + pub width: i32, + pub height: i32, + pub bytes: Vec, +} + +#[derive(Clone, Debug)] +pub enum IconUpdate { + Name(String), + Pixmap(Vec), } impl StatusNotifierItem { @@ -34,26 +37,13 @@ impl StatusNotifierItem { }; let item_proxy = StatusNotifierItemProxy::builder(connection) + // Status icons don't seem to report property changes the normal way... + .cache_properties(zbus::proxy::CacheProperties::No) .destination(dest.to_string())? .path(path.to_string())? .build() .await?; - let icon_name = item_proxy.icon_name().await.unwrap_or_default(); - let icon_pixmap = item_proxy - .icon_pixmap() - .await - .unwrap_or_default() - .into_iter() - .max_by_key(|i| (i.width, i.height)) - .map(|mut i| { - // Convert ARGB to RGBA - for pixel in i.bytes.chunks_exact_mut(4) { - pixel.rotate_left(1); - } - icon::from_raster_pixels(i.width as u32, i.height as u32, i.bytes) - }); - let menu_path = item_proxy.menu().await?; let menu_proxy = DBusMenuProxy::builder(connection) .destination(dest.to_string())? @@ -63,9 +53,7 @@ impl StatusNotifierItem { Ok(Self { name, - icon_name, - icon_pixmap, - _item_proxy: item_proxy, + item_proxy, menu_proxy, }) } @@ -74,19 +62,11 @@ impl StatusNotifierItem { &self.name } - pub fn icon_name(&self) -> &str { - &self.icon_name - } - - pub fn icon_pixmap(&self) -> Option<&icon::Handle> { - self.icon_pixmap.as_ref() - } - // TODO: Only fetch changed part of layout, if that's any faster pub fn layout_subscription(&self) -> iced::Subscription> { let menu_proxy = self.menu_proxy.clone(); Subscription::run_with_id( - format!("status-notifier-item-{}", &self.name), + format!("status-notifier-item-layout-{}", &self.name), async move { let initial = futures::stream::once(get_layout(menu_proxy.clone())); let layout_updated_stream = menu_proxy.receive_layout_updated().await.unwrap(); @@ -97,6 +77,38 @@ impl StatusNotifierItem { ) } + pub fn icon_subscription(&self) -> iced::Subscription { + fn icon_events<'a>( + item_proxy: StatusNotifierItemProxy<'static>, + ) -> impl futures::Stream + 'static { + async move { + let icon_name = item_proxy.icon_name().await; + let icon_pixmap = item_proxy.icon_pixmap().await; + futures::stream::iter( + [ + icon_name.map(IconUpdate::Name), + icon_pixmap.map(IconUpdate::Pixmap), + ] + .into_iter() + .filter_map(Result::ok), + ) + } + .flatten_stream() + } + + let item_proxy = self.item_proxy.clone(); + Subscription::run_with_id( + format!("status-notifier-item-icon-{}", &self.name), + async move { + let new_icon_stream = item_proxy.receive_new_icon().await.unwrap(); + futures::stream::once(async { () }) + .chain(new_icon_stream.map(|_| ())) + .flat_map(move |()| icon_events(item_proxy.clone())) + } + .flatten_stream(), + ) + } + pub fn menu_proxy(&self) -> &DBusMenuProxy<'static> { &self.menu_proxy } @@ -120,6 +132,9 @@ trait StatusNotifierItem { #[zbus(property)] fn menu(&self) -> zbus::Result; + + #[zbus(signal)] + fn new_icon(&self) -> zbus::Result<()>; } #[derive(Clone, Debug)]