eww/crates/notifier_host/src/icon.rs
Wölfchen 3b6f868ea6 chore: update gtk and related dependencies to 0.18
co-authored-by: Johan Geluk <johan@geluk.io>
2024-08-24 12:26:24 +02:00

207 lines
7.7 KiB
Rust

use crate::*;
use gtk::{self, prelude::*};
#[derive(thiserror::Error, Debug)]
enum IconError {
#[error("while fetching icon name: {0}")]
DBusIconName(#[source] zbus::Error),
#[error("while fetching icon theme path: {0}")]
DBusTheme(#[source] zbus::Error),
#[error("while fetching pixmap: {0}")]
DBusPixmap(#[source] zbus::Error),
#[error("loading icon from file {path:?}")]
LoadIconFromFile {
path: String,
#[source]
source: gtk::glib::Error,
},
#[error("loading icon {icon_name:?} from theme {}", .theme_path.as_ref().unwrap_or(&"(default)".to_owned()))]
LoadIconFromTheme {
icon_name: String,
theme_path: Option<String>,
#[source]
source: gtk::glib::Error,
},
#[error("no icon available")]
NotAvailable,
}
/// Get the fallback GTK icon, as a final fallback if the tray item has no icon.
async fn fallback_icon(size: i32, scale: i32) -> Option<gtk::gdk_pixbuf::Pixbuf> {
let theme = gtk::IconTheme::default().expect("Could not get default gtk theme");
match theme.load_icon_for_scale("image-missing", size, scale, gtk::IconLookupFlags::FORCE_SIZE) {
Ok(pb) => pb,
Err(e) => {
log::error!("failed to load \"image-missing\" from default theme: {}", e);
None
}
}
}
/// Load a pixbuf from StatusNotifierItem's [Icon format].
///
/// [Icon format]: https://freedesktop.org/wiki/Specifications/StatusNotifierItem/Icons/
fn icon_from_pixmap(width: i32, height: i32, mut data: Vec<u8>) -> gtk::gdk_pixbuf::Pixbuf {
// We need to convert data from ARGB32 to RGBA32, since that's the only one that gdk-pixbuf
// understands.
for chunk in data.chunks_exact_mut(4) {
let a = chunk[0];
let r = chunk[1];
let g = chunk[2];
let b = chunk[3];
chunk[0] = r;
chunk[1] = g;
chunk[2] = b;
chunk[3] = a;
}
gtk::gdk_pixbuf::Pixbuf::from_bytes(
&gtk::glib::Bytes::from_owned(data),
gtk::gdk_pixbuf::Colorspace::Rgb,
true,
8,
width,
height,
width * 4,
)
}
/// From a list of pixmaps, create an icon from the most appropriately sized one.
///
/// This function returns None if and only if no pixmaps are provided.
fn icon_from_pixmaps(pixmaps: Vec<(i32, i32, Vec<u8>)>, size: i32) -> Option<gtk::gdk_pixbuf::Pixbuf> {
pixmaps
.into_iter()
.max_by(|(w1, h1, _), (w2, h2, _)| {
// take smallest one bigger than requested size, otherwise take biggest
let a = size * size;
let a1 = w1 * h1;
let a2 = w2 * h2;
match (a1 >= a, a2 >= a) {
(true, true) => a2.cmp(&a1),
(true, false) => std::cmp::Ordering::Greater,
(false, true) => std::cmp::Ordering::Less,
(false, false) => a1.cmp(&a2),
}
})
.and_then(|(w, h, d)| {
let pixbuf = icon_from_pixmap(w, h, d);
if w != size || h != size {
pixbuf.scale_simple(size, size, gtk::gdk_pixbuf::InterpType::Bilinear)
} else {
Some(pixbuf)
}
})
}
/// Load an icon with a given name from either the default (if `theme_path` is `None`), or from the
/// theme at a path.
fn icon_from_name(
icon_name: &str,
theme_path: Option<&str>,
size: i32,
scale: i32,
) -> std::result::Result<gtk::gdk_pixbuf::Pixbuf, IconError> {
let theme = if let Some(path) = theme_path {
let theme = gtk::IconTheme::new();
theme.prepend_search_path(path);
theme
} else {
gtk::IconTheme::default().expect("Could not get default gtk theme")
};
match theme.load_icon_for_scale(icon_name, size, scale, gtk::IconLookupFlags::FORCE_SIZE) {
Ok(pb) => Ok(pb.expect("no pixbuf from theme.load_icon despite no error")),
Err(e) => Err(IconError::LoadIconFromTheme {
icon_name: icon_name.to_owned(),
theme_path: theme_path.map(str::to_owned),
source: e,
}),
}
}
pub async fn load_icon_from_sni(
sni: &proxy::StatusNotifierItemProxy<'_>,
size: i32,
scale: i32,
) -> Option<gtk::gdk_pixbuf::Pixbuf> {
// "Visualizations are encouraged to prefer icon names over icon pixmaps if both are
// available."
let scaled_size = size * scale;
// First, see if we can get an icon from the name they provide, using either the theme they
// specify or the default.
let icon_from_name: std::result::Result<gtk::gdk_pixbuf::Pixbuf, IconError> = (async {
// fetch icon name
let icon_name = sni.icon_name().await;
log::debug!("dbus: {} icon_name -> {:?}", sni.destination(), icon_name);
let icon_name = match icon_name {
Ok(s) if s.is_empty() => return Err(IconError::NotAvailable),
Ok(s) => s,
Err(e) => return Err(IconError::DBusIconName(e)),
};
// interpret it as an absolute path if we can
let icon_path = std::path::Path::new(&icon_name);
if icon_path.is_absolute() && icon_path.is_file() {
return gtk::gdk_pixbuf::Pixbuf::from_file_at_size(icon_path, scaled_size, scaled_size)
.map_err(|e| IconError::LoadIconFromFile { path: icon_name, source: e });
}
// otherwise, fetch icon theme and lookup using icon_from_name
let icon_theme_path = sni.icon_theme_path().await;
log::debug!("dbus: {} icon_theme_path -> {:?}", sni.destination(), icon_theme_path);
let icon_theme_path = match icon_theme_path {
Ok(p) if p.is_empty() => None,
Ok(p) => Some(p),
// treat property not existing as the same as it being empty i.e. to use the default
// system theme
Err(zbus::Error::FDO(e)) => match *e {
zbus::fdo::Error::UnknownProperty(_) | zbus::fdo::Error::InvalidArgs(_) => None,
// this error is reported by discord, blueman-applet
zbus::fdo::Error::Failed(msg) if msg == "error occurred in Get" => None,
_ => return Err(IconError::DBusTheme(zbus::Error::FDO(e))),
},
Err(e) => return Err(IconError::DBusTheme(e)),
};
let icon_theme_path: Option<&str> = match &icon_theme_path {
// this looks weird but this converts &String to &str
Some(s) => Some(s),
None => None,
};
icon_from_name(&icon_name, icon_theme_path, size, scale)
})
.await;
match icon_from_name {
Ok(p) => return Some(p), // got an icon!
Err(IconError::NotAvailable) => {} // this error is expected, don't log
Err(e) => log::warn!("failed to get icon by name for {}: {}", sni.destination(), e),
};
// Can't get it from name + theme, try the pixmap
let icon_from_pixmaps = match sni.icon_pixmap().await {
Ok(ps) => match icon_from_pixmaps(ps, scaled_size) {
Some(p) => Ok(p),
None => Err(IconError::NotAvailable),
},
Err(zbus::Error::FDO(e)) => match *e {
// property not existing is an expected error
zbus::fdo::Error::UnknownProperty(_) | zbus::fdo::Error::InvalidArgs(_) => Err(IconError::NotAvailable),
_ => Err(IconError::DBusPixmap(zbus::Error::FDO(e))),
},
Err(e) => Err(IconError::DBusPixmap(e)),
};
match icon_from_pixmaps {
Ok(p) => return Some(p),
Err(IconError::NotAvailable) => {}
Err(e) => log::warn!("failed to get icon pixmap for {}: {}", sni.destination(), e),
};
// Tray didn't provide a valid icon so use the default fallback one.
fallback_icon(size, scale).await
}