use std::borrow::Cow;
#[cfg(any(desktop, web, test))]
pub(crate) const FRUS_ICON_PNG: &[u8] = include_bytes!("../assets/icon.png");
#[derive(Clone, Debug, Default)]
pub enum AppIcon {
#[default]
Frus,
Png(Cow<'static, [u8]>),
None,
}
impl AppIcon {
pub fn frus() -> Self {
AppIcon::Frus
}
pub fn from_png(png: impl Into<Cow<'static, [u8]>>) -> Self {
AppIcon::Png(png.into())
}
pub fn none() -> Self {
AppIcon::None
}
#[cfg(any(desktop, web, test))]
pub(crate) fn png(&self) -> Option<&[u8]> {
match self {
AppIcon::Frus => Some(FRUS_ICON_PNG),
AppIcon::Png(bytes) => Some(bytes),
AppIcon::None => Option::None,
}
}
}
#[cfg(any(web, test))]
pub(crate) fn replaces_the_pages_icon(icon: &AppIcon, declared: Option<&str>) -> bool {
let page_has_one = declared.is_some_and(|href| !href.is_empty() && href != "data:,");
match icon {
AppIcon::Frus => !page_has_one,
AppIcon::Png(_) => true,
AppIcon::None => false,
}
}
#[cfg(desktop)]
pub(crate) fn window_icon(icon: &AppIcon) -> Option<winit::window::Icon> {
let png = icon.png()?;
let image = match frus_image::decode(png) {
Ok(image) => image,
Err(err) => {
log::warn!("the application's icon is not a PNG that can be read: {err}");
return Option::None;
}
};
match winit::window::Icon::from_rgba(image.rgba().to_vec(), image.width(), image.height()) {
Ok(icon) => Some(icon),
Err(err) => {
log::warn!("the application's icon cannot be used as a window icon: {err}");
Option::None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(desktop)]
#[test]
fn the_default_icon_is_the_logo_at_256_pixels_with_a_transparent_background() {
let image = frus_image::decode(FRUS_ICON_PNG).expect("the embedded icon decodes");
assert_eq!((image.width(), image.height()), (256, 256));
let alphas: Vec<u8> = image.rgba().iter().skip(3).step_by(4).copied().collect();
assert!(alphas.contains(&255), "a drawn mark");
assert!(alphas.contains(&0), "and a transparent corner");
assert!(
FRUS_ICON_PNG.len() < 64 * 1024,
"the icon is a few tens of kilobytes, not the artwork's {} kB",
FRUS_ICON_PNG.len() / 1024
);
}
#[test]
fn what_each_choice_shows() {
assert_eq!(AppIcon::default().png(), Some(FRUS_ICON_PNG));
assert_eq!(AppIcon::frus().png(), Some(FRUS_ICON_PNG));
assert_eq!(AppIcon::from_png(&b"png"[..]).png(), Some(&b"png"[..]));
assert_eq!(AppIcon::none().png(), Option::None);
}
#[cfg(desktop)]
#[test]
fn a_window_icon_is_made_for_a_png_and_only_for_a_png() {
assert!(window_icon(&AppIcon::frus()).is_some());
assert!(window_icon(&AppIcon::none()).is_none());
assert!(window_icon(&AppIcon::from_png(&b"not an image"[..])).is_none());
assert!(window_icon(&AppIcon::from_png(FRUS_ICON_PNG)).is_some());
}
#[test]
fn the_default_yields_to_a_page_that_has_an_icon() {
let frus = AppIcon::frus();
assert!(
replaces_the_pages_icon(&frus, Option::None),
"no icon declared"
);
assert!(replaces_the_pages_icon(&frus, Some("")), "an empty one");
assert!(
replaces_the_pages_icon(&frus, Some("data:,")),
"the page's way of saying none"
);
assert!(
!replaces_the_pages_icon(&frus, Some("/favicon.png")),
"the page's own"
);
let own = AppIcon::from_png(&b"png"[..]);
assert!(
replaces_the_pages_icon(&own, Some("/favicon.png")),
"the application's own wins"
);
assert!(!replaces_the_pages_icon(&AppIcon::none(), Option::None));
assert!(!replaces_the_pages_icon(
&AppIcon::none(),
Some("/favicon.png")
));
}
}