Use application artwork in the running window tray
This commit is contained in:
1 parent
81b99709bb
commit
78ce1ed590
4 files changed
+145
-19
No files matched your search
@@ -0,0 +1,133 @@
|
||||
//! Application artwork is resolved off the UI thread, then cached by app ID.
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use cosmic::widget::icon;
|
||||
use freedesktop_desktop_entry as fde;
|
||||
|
||||
pub(super) fn fallback() -> icon::Handle {
|
||||
icon::from_svg_bytes(ferese_theme::icons::WINDOW).symbolic(true)
|
||||
}
|
||||
|
||||
pub(super) fn resolve(entries: &[fde::DesktopEntry], app_id: &str) -> icon::Handle {
|
||||
path(entries, app_id, |name| {
|
||||
icon::from_name(name.to_owned())
|
||||
.prefer_svg(true)
|
||||
.size(32)
|
||||
// A shortened name could identify a different application.
|
||||
.fallback(None)
|
||||
.path()
|
||||
})
|
||||
.map(icon::from_path)
|
||||
.unwrap_or_else(fallback)
|
||||
}
|
||||
|
||||
fn path(
|
||||
entries: &[fde::DesktopEntry],
|
||||
app_id: &str,
|
||||
mut lookup: impl FnMut(&str) -> Option<PathBuf>,
|
||||
) -> Option<PathBuf> {
|
||||
let entry = fde::find_app_by_id(entries, fde::unicase::Ascii::new(app_id));
|
||||
let source = entry
|
||||
.and_then(fde::DesktopEntry::icon)
|
||||
.filter(|s| !s.is_empty());
|
||||
let declared = source.and_then(|source| {
|
||||
if Path::new(source).is_absolute() {
|
||||
Some(PathBuf::from(source))
|
||||
} else {
|
||||
lookup(source)
|
||||
}
|
||||
});
|
||||
declared.filter(|path| path.is_file()).or_else(|| {
|
||||
// Some applications install an app-ID icon without an Icon= entry.
|
||||
// Also try the canonical desktop ID for StartupWMClass aliases.
|
||||
[Some(app_id), entry.map(|entry| entry.appid.as_str())]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter(|id| !id.is_empty() && !id.contains('/'))
|
||||
.find_map(|id| lookup(id).filter(|path| path.is_file()))
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn entry(directory: &Path, id: &str, fields: &str) -> fde::DesktopEntry {
|
||||
let path = directory.join(format!("{id}.desktop"));
|
||||
std::fs::write(
|
||||
&path,
|
||||
format!("[Desktop Entry]\nType=Application\nName=Test\n{fields}\n"),
|
||||
)
|
||||
.unwrap();
|
||||
fde::DesktopEntry::from_path(path, Some(&[] as &[&str])).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn declared_artwork_wins_and_absolute_raster_icons_work() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let png = dir.path().join("own.png");
|
||||
std::fs::write(&png, b"fixture").unwrap();
|
||||
let entries = [entry(
|
||||
dir.path(),
|
||||
"test.app",
|
||||
&format!("Icon={}", png.display()),
|
||||
)];
|
||||
assert_eq!(
|
||||
path(&entries, "test.app", |_| panic!(
|
||||
"absolute icon needs no lookup"
|
||||
)),
|
||||
Some(png)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_or_broken_declarations_use_the_app_id_and_wm_class_alias() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let svg = dir.path().join("own.svg");
|
||||
std::fs::write(&svg, b"fixture").unwrap();
|
||||
for fields in ["", "Icon=missing-icon", "Icon=/missing/icon.png"] {
|
||||
let entries = [entry(
|
||||
dir.path(),
|
||||
"test.app",
|
||||
&format!("StartupWMClass=Alias\n{fields}"),
|
||||
)];
|
||||
let mut names = Vec::new();
|
||||
assert_eq!(
|
||||
path(&entries, "Alias", |name| {
|
||||
names.push(name.to_owned());
|
||||
(name == "test.app").then(|| svg.clone())
|
||||
}),
|
||||
Some(svg.clone())
|
||||
);
|
||||
assert!(names.iter().any(|name| name == "Alias"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_applications_never_use_the_ferese_brand() {
|
||||
assert!(path(&[], "unknown", |_| None).is_none());
|
||||
let icon = fallback();
|
||||
assert!(icon.symbolic);
|
||||
match icon.data {
|
||||
icon::Data::Svg(handle) => {
|
||||
assert_ne!(
|
||||
handle,
|
||||
cosmic::widget::svg::Handle::from_memory(ferese_theme::icons::APPLICATION)
|
||||
);
|
||||
}
|
||||
_ => panic!("fallback must be the bundled window glyph"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "inspect desktop/icon lookup using the caller's XDG environment"]
|
||||
fn inspect_installed_application_icons() {
|
||||
let entries: Vec<_> = fde::Iter::new(fde::default_paths())
|
||||
.entries::<&str>(Some(&[]))
|
||||
.collect();
|
||||
let ids = std::env::var("FERESE_TEST_APP_IDS").unwrap();
|
||||
for id in ids.split(',') {
|
||||
eprintln!("APP_ICON {id}: {:?}", resolve(&entries, id));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ mod tray;
|
||||
use bar::{bar_content, output_bar_hidden};
|
||||
use surfaces::{EffectsBinding, EffectsContext, native_wayland_surface};
|
||||
|
||||
mod application_icons;
|
||||
mod capture_status;
|
||||
mod clock;
|
||||
mod compositor_ipc;
|
||||
|
||||
@@ -89,20 +89,7 @@ fn discover_icons(entries: Option<Arc<Vec<fde::DesktopEntry>>>, app_ids: Vec<Str
|
||||
let icons = app_ids
|
||||
.into_iter()
|
||||
.map(|app_id| {
|
||||
let entry = fde::find_app_by_id(&entries, fde::unicase::Ascii::new(app_id.as_str()));
|
||||
let path = entry.and_then(fde::DesktopEntry::icon).and_then(|source| {
|
||||
if source.starts_with('/') {
|
||||
Some(std::path::PathBuf::from(source))
|
||||
} else {
|
||||
icon::from_name(source).prefer_svg(true).size(32).path()
|
||||
}
|
||||
});
|
||||
// Named::handle returns an empty SVG when neither an application
|
||||
// icon nor a theme fallback exists. Keep every live window visible.
|
||||
let handle = path
|
||||
.filter(|path| path.is_file())
|
||||
.map(icon::from_path)
|
||||
.unwrap_or_else(|| icon::from_svg_bytes(ferese_theme::icons::APPLICATION));
|
||||
let handle = crate::application_icons::resolve(&entries, &app_id);
|
||||
(app_id, handle)
|
||||
})
|
||||
.collect();
|
||||
@@ -533,7 +520,7 @@ impl WorkspaceUi {
|
||||
self.icons
|
||||
.get(id)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| icon::from_svg_bytes(ferese_theme::icons::APPLICATION))
|
||||
.unwrap_or_else(crate::application_icons::fallback)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -542,7 +529,7 @@ impl WorkspaceUi {
|
||||
self.icons
|
||||
.get(app_id)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| icon::from_svg_bytes(ferese_theme::icons::APPLICATION))
|
||||
.unwrap_or_else(crate::application_icons::fallback)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user