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.
This commit is contained in:
Ian Douglas Scott 2025-03-17 13:51:56 -07:00 committed by Ian Douglas Scott
parent 7fdaf839e0
commit 2a939e5a11
2 changed files with 83 additions and 38 deletions

View file

@ -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<Layout, String>),
Icon(IconUpdate),
Click(i32, bool),
}
@ -19,6 +20,9 @@ pub struct State {
item: StatusNotifierItem,
layout: Option<Layout>,
expanded: Option<i32>,
icon_name: String,
// TODO handle icon with multiple sizes?
icon_pixmap: Option<icon::Handle>,
}
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<Msg> {
@ -84,7 +111,10 @@ impl State {
}
pub fn subscription(&self) -> iced::Subscription<Msg> {
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) {

View file

@ -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<icon::Handle>,
_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<u8>,
pub width: i32,
pub height: i32,
pub bytes: Vec<u8>,
}
#[derive(Clone, Debug)]
pub enum IconUpdate {
Name(String),
Pixmap(Vec<Icon>),
}
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<Result<Layout, String>> {
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<IconUpdate> {
fn icon_events<'a>(
item_proxy: StatusNotifierItemProxy<'static>,
) -> impl futures::Stream<Item = IconUpdate> + '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<zvariant::OwnedObjectPath>;
#[zbus(signal)]
fn new_icon(&self) -> zbus::Result<()>;
}
#[derive(Clone, Debug)]