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, #[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 { 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) -> 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( >k::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)>, size: i32) -> Option { 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 { 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 { // "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 = (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 }