use tiny_skia::{FillRule, Paint, Path, PathBuilder, Pixmap, Transform};
use crate::render::Bounds;
#[path = "builtin_icon_paths.rs"]
mod builtin_icon_paths;
use builtin_icon_paths::{Cmd, ICONS, RawIcon};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BuiltinIcon {
Clock,
BatteryLow,
BatteryHalf,
BatteryFull,
BatteryCharging,
Backlight(u8),
NetworkWireless,
NetworkWired,
System,
Memory,
Bluetooth,
Updates,
Notifications,
NotificationsOff,
HypridleActive,
HypridleInactive,
Power,
PowerProfilePerformance,
PowerProfileBalanced,
PowerProfileSaver,
VolumeLow,
VolumeMedium,
VolumeHigh,
VolumeMuted,
}
impl BuiltinIcon {
fn stem(self) -> &'static str {
match self {
BuiltinIcon::Clock => "clock-filled",
BuiltinIcon::BatteryLow => "battery-low2-filled",
BuiltinIcon::BatteryHalf => "battery-half2-filled",
BuiltinIcon::BatteryFull => "battery-full2-filled",
BuiltinIcon::BatteryCharging => "battery-charge2-filled",
BuiltinIcon::Backlight(level) => match level {
0 => "bright-empty",
1 => "bright-1",
2 => "bright-2",
3 => "bright-3",
4 => "bright-4",
5 => "bright-5",
_ => "bright-full",
},
BuiltinIcon::NetworkWireless => "wifi-filled",
BuiltinIcon::NetworkWired => "ethernet-filled",
BuiltinIcon::System => "cpu-filled",
BuiltinIcon::Memory => "memory-filled",
BuiltinIcon::Bluetooth => "bluetooth-outline",
BuiltinIcon::Updates => "box5-filled",
BuiltinIcon::Notifications => "bell-filled",
BuiltinIcon::NotificationsOff => "bell-off2-filled",
BuiltinIcon::HypridleActive => "lock-open-filled",
BuiltinIcon::HypridleInactive => "lock-filled",
BuiltinIcon::Power => "power-off-filled",
BuiltinIcon::PowerProfilePerformance => "bolt-lightning-filled",
BuiltinIcon::PowerProfileBalanced => "balance-filled",
BuiltinIcon::PowerProfileSaver => "feather-filled",
BuiltinIcon::VolumeLow => "volume-down-filled",
BuiltinIcon::VolumeMedium => "volume-filled",
BuiltinIcon::VolumeHigh => "volume-up-filled",
BuiltinIcon::VolumeMuted => "volume-x-filled",
}
}
fn raw(self) -> Option<&'static RawIcon> {
let stem = self.stem();
ICONS.iter().find(|icon| icon.name == stem)
}
}
fn build_path(cmds: &[Cmd]) -> Option<Path> {
let mut pb = PathBuilder::new();
for cmd in cmds {
match *cmd {
Cmd::Move(x, y) => pb.move_to(x, y),
Cmd::Line(x, y) => pb.line_to(x, y),
Cmd::Cubic(x1, y1, x2, y2, x, y) => pb.cubic_to(x1, y1, x2, y2, x, y),
Cmd::Close => pb.close(),
}
}
pb.finish()
}
pub fn draw_into(pixmap: &mut Pixmap, icon: BuiltinIcon, bounds: Bounds, color: (u8, u8, u8, u8)) {
let Some(raw) = icon.raw() else {
return;
};
if raw.view_w <= 0.0 || raw.view_h <= 0.0 {
return;
}
let scale = (bounds.width as f32 / raw.view_w).min(bounds.height as f32 / raw.view_h);
let draw_w = raw.view_w * scale;
let draw_h = raw.view_h * scale;
let tx = bounds.x as f32 + (bounds.width as f32 - draw_w) / 2.0;
let ty = bounds.y as f32 + (bounds.height as f32 - draw_h) / 2.0;
let transform = Transform::from_row(scale, 0.0, 0.0, scale, tx, ty);
let (r, g, b, a) = color;
let mut paint = Paint::default();
paint.set_color_rgba8(r, g, b, a);
paint.anti_alias = true;
for sub in raw.paths {
let Some(path) = build_path(sub.cmds) else {
continue;
};
let rule = if sub.even_odd {
FillRule::EvenOdd
} else {
FillRule::Winding
};
pixmap.fill_path(&path, &paint, rule, transform, None);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_semantic_icon_resolves_to_artwork() {
let mut icons = vec![
BuiltinIcon::Clock,
BuiltinIcon::BatteryLow,
BuiltinIcon::BatteryHalf,
BuiltinIcon::BatteryFull,
BuiltinIcon::BatteryCharging,
BuiltinIcon::NetworkWireless,
BuiltinIcon::NetworkWired,
BuiltinIcon::System,
BuiltinIcon::Memory,
BuiltinIcon::Bluetooth,
BuiltinIcon::Updates,
BuiltinIcon::Notifications,
BuiltinIcon::NotificationsOff,
BuiltinIcon::HypridleActive,
BuiltinIcon::HypridleInactive,
BuiltinIcon::Power,
BuiltinIcon::PowerProfilePerformance,
BuiltinIcon::PowerProfileBalanced,
BuiltinIcon::PowerProfileSaver,
BuiltinIcon::VolumeLow,
BuiltinIcon::VolumeMedium,
BuiltinIcon::VolumeHigh,
BuiltinIcon::VolumeMuted,
];
icons.extend((0..=7).map(BuiltinIcon::Backlight));
for icon in icons {
assert!(
icon.raw().is_some(),
"no artwork for {icon:?} (stem {:?})",
icon.stem()
);
}
}
#[test]
fn backlight_ramp_saturates_at_full() {
assert_eq!(BuiltinIcon::Backlight(6).stem(), "bright-full");
assert_eq!(BuiltinIcon::Backlight(99).stem(), "bright-full");
assert_eq!(BuiltinIcon::Backlight(0).stem(), "bright-empty");
}
#[test]
fn draw_into_fills_pixels_in_the_requested_color() {
let mut pixmap = Pixmap::new(32, 32).unwrap();
draw_into(
&mut pixmap,
BuiltinIcon::System,
Bounds::new(0, 0, 32, 32),
(0xFF, 0x00, 0x00, 0xFF),
);
let painted = pixmap
.data()
.chunks_exact(4)
.any(|px| px[3] > 0x40 && px[0] > 0x80);
assert!(painted, "icon fill produced no visible pixels");
}
#[test]
fn draw_into_leaves_a_zero_scale_slot_untouched() {
let mut pixmap = Pixmap::new(16, 16).unwrap();
draw_into(
&mut pixmap,
BuiltinIcon::Clock,
Bounds::new(0, 0, 0, 16),
(0xFF, 0xFF, 0xFF, 0xFF),
);
assert!(pixmap.data().iter().all(|&b| b == 0));
}
}