207 lines
7.7 KiB
Rust
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(
|
|
>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<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
|
|
}
|