feat: add media controls and fixed icon resolution
Code Quality / quality-checks (push) Has been cancelled
Security Scan / security-audit (push) Has been cancelled

- Implement Wayland pointer handling for interactive media buttons.
- Add pure-Rust SVG loading via resvg.
- Refactor status indicators for consistent icon and layout handling.
This commit is contained in:
2026-04-10 17:32:52 +02:00
parent 49438ecd62
commit 61daf52906
5 changed files with 684 additions and 94 deletions
Generated
+198 -3
View File
@@ -72,6 +72,18 @@ version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "arrayref"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb"
[[package]]
name = "arrayvec"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]] [[package]]
name = "async-broadcast" name = "async-broadcast"
version = "0.5.1" version = "0.5.1"
@@ -259,6 +271,12 @@ version = "0.21.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]] [[package]]
name = "bindgen" name = "bindgen"
version = "0.69.5" version = "0.69.5"
@@ -606,6 +624,12 @@ dependencies = [
"syn 1.0.109", "syn 1.0.109",
] ]
[[package]]
name = "data-url"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376"
[[package]] [[package]]
name = "dbus" name = "dbus"
version = "0.9.10" version = "0.9.10"
@@ -762,6 +786,15 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "euclid"
version = "0.22.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06"
dependencies = [
"num-traits",
]
[[package]] [[package]]
name = "event-listener" name = "event-listener"
version = "2.5.3" version = "2.5.3"
@@ -846,6 +879,12 @@ dependencies = [
"miniz_oxide", "miniz_oxide",
] ]
[[package]]
name = "float-cmp"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4"
[[package]] [[package]]
name = "fnv" name = "fnv"
version = "1.0.7" version = "1.0.7"
@@ -1440,11 +1479,17 @@ dependencies = [
"byteorder-lite", "byteorder-lite",
"moxcms", "moxcms",
"num-traits", "num-traits",
"png", "png 0.18.1",
"zune-core", "zune-core",
"zune-jpeg", "zune-jpeg",
] ]
[[package]]
name = "imagesize"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "029d73f573d8e8d63e6d5020011d3255b28c3ba85d6cf870a07184ed23de9284"
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.13.0" version = "2.13.0"
@@ -1538,6 +1583,17 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "kurbo"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c62026ae44756f8a599ba21140f350303d4f08dcdcc71b5ad9c9bb8128c13c62"
dependencies = [
"arrayvec",
"euclid",
"smallvec",
]
[[package]] [[package]]
name = "lazy_static" name = "lazy_static"
version = "1.5.0" version = "1.5.0"
@@ -1850,6 +1906,12 @@ version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "pico-args"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315"
[[package]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.2.17" version = "0.2.17"
@@ -1879,6 +1941,19 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
[[package]]
name = "png"
version = "0.17.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526"
dependencies = [
"bitflags 1.3.2",
"crc32fast",
"fdeflate",
"flate2",
"miniz_oxide",
]
[[package]] [[package]]
name = "png" name = "png"
version = "0.18.1" version = "0.18.1"
@@ -2103,7 +2178,7 @@ version = "0.11.27"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62"
dependencies = [ dependencies = [
"base64", "base64 0.21.7",
"bytes", "bytes",
"encoding_rs", "encoding_rs",
"futures-core", "futures-core",
@@ -2138,6 +2213,29 @@ dependencies = [
"winreg", "winreg",
] ]
[[package]]
name = "resvg"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "944d052815156ac8fa77eaac055220e95ba0b01fa8887108ca710c03805d9051"
dependencies = [
"log",
"pico-args",
"rgb",
"svgtypes",
"tiny-skia",
"usvg",
]
[[package]]
name = "rgb"
version = "0.8.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4"
dependencies = [
"bytemuck",
]
[[package]] [[package]]
name = "ring" name = "ring"
version = "0.17.14" version = "0.17.14"
@@ -2152,6 +2250,12 @@ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "roxmltree"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
[[package]] [[package]]
name = "rpassword" name = "rpassword"
version = "6.0.1" version = "6.0.1"
@@ -2234,12 +2338,15 @@ dependencies = [
"pangocairo", "pangocairo",
"rand", "rand",
"reqwest", "reqwest",
"resvg",
"serde", "serde",
"smithay-client-toolkit", "smithay-client-toolkit",
"thiserror", "thiserror",
"tiny-skia",
"tokio", "tokio",
"toml 1.0.6+spec-1.1.0", "toml 1.0.6+spec-1.1.0",
"upower_dbus", "upower_dbus",
"usvg",
"wayland-client", "wayland-client",
"wayland-protocols", "wayland-protocols",
"wayland-protocols-wlr", "wayland-protocols-wlr",
@@ -2267,7 +2374,7 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c"
dependencies = [ dependencies = [
"base64", "base64 0.21.7",
] ]
[[package]] [[package]]
@@ -2416,6 +2523,21 @@ version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
[[package]]
name = "simplecss"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c"
dependencies = [
"log",
]
[[package]]
name = "siphasher"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e"
[[package]] [[package]]
name = "slab" name = "slab"
version = "0.4.12" version = "0.4.12"
@@ -2498,12 +2620,31 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "strict-num"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
dependencies = [
"float-cmp",
]
[[package]] [[package]]
name = "strsim" name = "strsim"
version = "0.10.0" version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "svgtypes"
version = "0.15.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68c7541fff44b35860c1a7a47a7cadf3e4a304c457b58f9870d9706ece028afc"
dependencies = [
"kurbo",
"siphasher",
]
[[package]] [[package]]
name = "syn" name = "syn"
version = "0.11.11" version = "0.11.11"
@@ -2636,6 +2777,32 @@ dependencies = [
"syn 2.0.117", "syn 2.0.117",
] ]
[[package]]
name = "tiny-skia"
version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab"
dependencies = [
"arrayref",
"arrayvec",
"bytemuck",
"cfg-if",
"log",
"png 0.17.16",
"tiny-skia-path",
]
[[package]]
name = "tiny-skia-path"
version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93"
dependencies = [
"arrayref",
"bytemuck",
"strict-num",
]
[[package]] [[package]]
name = "tinystr" name = "tinystr"
version = "0.8.2" version = "0.8.2"
@@ -2901,6 +3068,28 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "usvg"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b84ea542ae85c715f07b082438a4231c3760539d902e11d093847a0b22963032"
dependencies = [
"base64 0.22.1",
"data-url",
"flate2",
"imagesize",
"kurbo",
"log",
"pico-args",
"roxmltree",
"simplecss",
"siphasher",
"strict-num",
"svgtypes",
"tiny-skia-path",
"xmlwriter",
]
[[package]] [[package]]
name = "utf8_iter" name = "utf8_iter"
version = "1.0.4" version = "1.0.4"
@@ -3578,6 +3767,12 @@ dependencies = [
"bytemuck", "bytemuck",
] ]
[[package]]
name = "xmlwriter"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9"
[[package]] [[package]]
name = "yoke" name = "yoke"
version = "0.8.1" version = "0.8.1"
+10 -7
View File
@@ -30,13 +30,16 @@ num-traits = { version = "0.2" }
rand = { version = "0.8" } rand = { version = "0.8" }
reqwest = { version = "0.11", default-features = false, features = ["blocking", "rustls-tls"], optional = true } reqwest = { version = "0.11", default-features = false, features = ["blocking", "rustls-tls"], optional = true }
image = { version = "0.25", default-features = false, features = ["png", "jpeg"] } image = { version = "0.25", default-features = false, features = ["png", "jpeg"] }
fastblur = "0.1" fastblur = "0.1"
pam-client = "0.5" pam-client = "0.5"
thiserror = "1.0" thiserror = "1.0"
bytemuck = "1.14" bytemuck = "1.14"
calloop = "0.13" calloop = "0.13"
xkbcommon = "0.7" xkbcommon = "0.7"
calloop-wayland-source = "0.3" calloop-wayland-source = "0.3"
resvg = { version = "0.42", default-features = false }
usvg = { version = "0.42", default-features = false }
tiny-skia = "0.11"
[features] [features]
default = ["networking"] default = ["networking"]
+44 -1
View File
@@ -35,6 +35,7 @@ use smithay_client_toolkit::{
registry_handlers, registry_handlers,
seat::{ seat::{
keyboard::{KeyEvent, KeyboardHandler, Modifiers}, keyboard::{KeyEvent, KeyboardHandler, Modifiers},
pointer::{PointerEvent, PointerEventKind, PointerHandler},
SeatHandler, SeatState, SeatHandler, SeatState,
}, },
session_lock::{ session_lock::{
@@ -248,6 +249,8 @@ impl ProvidesRegistryState for WaylandLock {
registry_handlers![OutputState, SeatState]; registry_handlers![OutputState, SeatState];
} }
impl WaylandLock {}
impl CompositorHandler for WaylandLock { impl CompositorHandler for WaylandLock {
fn scale_factor_changed( fn scale_factor_changed(
&mut self, &mut self,
@@ -432,7 +435,7 @@ impl SeatHandler for WaylandLock {
) { ) {
if capability == smithay_client_toolkit::seat::Capability::Keyboard { if capability == smithay_client_toolkit::seat::Capability::Keyboard {
let _ = self.seat_state.get_keyboard_with_repeat( let _ = self.seat_state.get_keyboard_with_repeat(
qh, &qh,
&seat, &seat,
None, None,
self.loop_handle.clone(), self.loop_handle.clone(),
@@ -441,6 +444,9 @@ impl SeatHandler for WaylandLock {
}), }),
); );
} }
if capability == smithay_client_toolkit::seat::Capability::Pointer {
let _ = self.seat_state.get_pointer(&qh, &seat);
}
} }
fn remove_capability( fn remove_capability(
&mut self, &mut self,
@@ -571,6 +577,43 @@ impl wayland_client::Dispatch<wayland_client::protocol::wl_registry::WlRegistry,
} }
} }
impl PointerHandler for WaylandLock {
fn pointer_frame(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_pointer: &wayland_client::protocol::wl_pointer::WlPointer,
events: &[PointerEvent],
) {
for event in events {
if let PointerEventKind::Press { button, .. } = event.kind {
if button == 0x110 {
let (x, y) = event.position;
if let Ok(lm) = self.lock_manager.lock() {
for surface in &lm.surfaces {
if surface.matches_surface(&event.surface) {
for (action, rx, ry, rw, rh) in &surface.renderer.media_rects {
if x >= *rx && x <= rx + rw && y >= *ry && y <= ry + rh {
match action.as_str() {
"play" => self.system_manager.media_play_pause(),
"stop" => self.system_manager.media_stop(),
"next" => self.system_manager.media_next(),
"prev" => self.system_manager.media_prev(),
_ => {}
}
return;
}
}
}
}
}
}
}
}
}
}
smithay_client_toolkit::delegate_pointer!(WaylandLock);
smithay_client_toolkit::delegate_compositor!(WaylandLock); smithay_client_toolkit::delegate_compositor!(WaylandLock);
smithay_client_toolkit::delegate_output!(WaylandLock); smithay_client_toolkit::delegate_output!(WaylandLock);
smithay_client_toolkit::delegate_shm!(WaylandLock); smithay_client_toolkit::delegate_shm!(WaylandLock);
+393 -52
View File
@@ -34,6 +34,7 @@ pub struct Renderer {
media_stop_icon_surface: Option<ImageSurface>, media_stop_icon_surface: Option<ImageSurface>,
media_play_icon_surface: Option<ImageSurface>, media_play_icon_surface: Option<ImageSurface>,
media_next_icon_surface: Option<ImageSurface>, media_next_icon_surface: Option<ImageSurface>,
pub media_rects: Vec<(String, f64, f64, f64, f64)>,
} }
impl Renderer { impl Renderer {
@@ -73,6 +74,7 @@ impl Renderer {
media_stop_icon_surface: None, media_stop_icon_surface: None,
media_play_icon_surface: None, media_play_icon_surface: None,
media_next_icon_surface: None, media_next_icon_surface: None,
media_rects: Vec::new(),
}; };
renderer.load_icons(); renderer.load_icons();
@@ -80,37 +82,294 @@ impl Renderer {
} }
fn load_icons(&mut self) { fn load_icons(&mut self) {
if let Some(ref path) = self.config.wifi_icon { log::info!("Attempting to load status icons...");
self.wifi_icon_surface = self.load_icon(path); let wifi_names = [
"network-wireless-signal-excellent-symbolic",
"network-wireless-signal-excellent",
"network-wireless-symbolic",
"network-wireless",
];
let wifi_path = self
.config
.wifi_icon
.clone()
.or_else(|| {
for name in &wifi_names {
if let Some(path) = self.find_system_icon(name) {
return Some(path);
} }
if let Some(ref path) = self.config.bluetooth_icon {
self.bluetooth_icon_surface = self.load_icon(path);
} }
if let Some(ref path) = self.config.battery_icon { None
self.battery_icon_surface = self.load_icon(path); })
.unwrap_or_default();
if !wifi_path.is_empty() {
log::info!("Resolved WiFi icon path: {}", wifi_path);
self.wifi_icon_surface = self.load_icon(&wifi_path);
} else {
log::warn!("WiFi icon not found in system themes.");
} }
if let Some(ref path) = self.config.media_prev_icon {
self.media_prev_icon_surface = self.load_icon(path); let bt_names = [
"bluetooth-active-symbolic",
"bluetooth-symbolic",
"bluetooth-active",
"bluetooth",
];
let bt_path = self
.config
.bluetooth_icon
.clone()
.or_else(|| {
for name in &bt_names {
if let Some(path) = self.find_system_icon(name) {
return Some(path);
} }
if let Some(ref path) = self.config.media_stop_icon {
self.media_stop_icon_surface = self.load_icon(path);
} }
if let Some(ref path) = self.config.media_play_icon { None
self.media_play_icon_surface = self.load_icon(path); })
.unwrap_or_default();
if !bt_path.is_empty() {
log::info!("Resolved Bluetooth icon path: {}", bt_path);
self.bluetooth_icon_surface = self.load_icon(&bt_path);
} else {
log::warn!("Bluetooth icon not found in system themes.");
} }
if let Some(ref path) = self.config.media_next_icon {
self.media_next_icon_surface = self.load_icon(path); let batt_names = [
"battery-level-100-symbolic",
"battery-full-symbolic",
"battery-full",
"battery-level-100",
"battery",
"battery-symbolic",
];
let batt_path = self
.config
.battery_icon
.clone()
.or_else(|| {
for name in &batt_names {
if let Some(path) = self.find_system_icon(name) {
return Some(path);
} }
} }
None
})
.unwrap_or_default();
if !batt_path.is_empty() {
log::info!("Resolved Battery icon path: {}", batt_path);
self.battery_icon_surface = self.load_icon(&batt_path);
} else {
log::warn!("Battery icon not found in system themes.");
}
let prev_names = [
"media-skip-backward-symbolic",
"media-skip-backward",
"media-playlist-repeat-symbolic",
];
let prev_path = self
.config
.media_prev_icon
.clone()
.or_else(|| {
for name in &prev_names {
if let Some(path) = self.find_system_icon(name) {
return Some(path);
}
}
None
})
.unwrap_or_default();
if !prev_path.is_empty() {
log::debug!("Resolved Media Prev icon path: {}", prev_path);
self.media_prev_icon_surface = self.load_icon(&prev_path);
}
let stop_names = ["media-playback-stop-symbolic", "media-playback-stop"];
let stop_path = self
.config
.media_stop_icon
.clone()
.or_else(|| {
for name in &stop_names {
if let Some(path) = self.find_system_icon(name) {
return Some(path);
}
}
None
})
.unwrap_or_default();
if !stop_path.is_empty() {
log::debug!("Resolved Media Stop icon path: {}", stop_path);
self.media_stop_icon_surface = self.load_icon(&stop_path);
}
let play_names = ["media-playback-start-symbolic", "media-playback-start"];
let play_path = self
.config
.media_play_icon
.clone()
.or_else(|| {
for name in &play_names {
if let Some(path) = self.find_system_icon(name) {
return Some(path);
}
}
None
})
.unwrap_or_default();
if !play_path.is_empty() {
log::debug!("Resolved Media Play icon path: {}", play_path);
self.media_play_icon_surface = self.load_icon(&play_path);
}
let next_names = ["media-skip-forward-symbolic", "media-skip-forward"];
let next_path = self
.config
.media_next_icon
.clone()
.or_else(|| {
for name in &next_names {
if let Some(path) = self.find_system_icon(name) {
return Some(path);
}
}
None
})
.unwrap_or_default();
if !next_path.is_empty() {
log::debug!("Resolved Media Next icon path: {}", next_path);
self.media_next_icon_surface = self.load_icon(&next_path);
}
}
fn find_system_icon(&self, name: &str) -> Option<String> {
let data_dirs = std::env::var("XDG_DATA_DIRS").unwrap_or_default();
let mut search_paths = Vec::new();
for dir in data_dirs.split(':') {
let p = std::path::PathBuf::from(dir).join("icons");
if p.exists() {
search_paths.push(p);
}
}
let sys_path = std::path::PathBuf::from("/run/current-system/sw/share/icons");
if sys_path.exists() {
search_paths.push(sys_path);
}
let usr_path = std::path::PathBuf::from("/usr/share/icons");
if usr_path.exists() {
search_paths.push(usr_path);
}
let themes = [
"WhiteSur",
"WhiteSur-dark",
"WhiteSur-light",
"Adwaita",
"hicolor",
"breeze",
"Papirus",
];
let categories = [
"status/symbolic",
"actions/symbolic",
"devices/symbolic",
"status/24",
"status/22",
"status/16",
"status",
"actions",
"devices",
"symbolic/status",
"symbolic/actions",
"symbolic/devices",
"24x24/status",
"22x22/status",
"16x16/status",
"48x48/status",
];
for base in &search_paths {
for theme in &themes {
for cat in &categories {
for ext in [".svg", ".png"] {
let icon_path = base.join(theme).join(cat).join(format!("{}{}", name, ext));
if icon_path.exists() {
log::debug!("Icon found: {:?}", icon_path);
return Some(icon_path.to_string_lossy().into_owned());
}
}
}
}
}
for base in &search_paths {
for theme in &themes {
let theme_root = base.join(theme);
if !theme_root.exists() {
continue;
}
if let Ok(entries) = std::fs::read_dir(&theme_root) {
for entry in entries.flatten() {
if entry.path().is_dir() {
for ext in [".svg", ".png"] {
let icon_path = entry.path().join(format!("{}{}", name, ext));
if icon_path.exists() {
log::debug!("Icon found (deep): {:?}", icon_path);
return Some(icon_path.to_string_lossy().into_owned());
}
if let Ok(sub_entries) = std::fs::read_dir(entry.path()) {
for sub_entry in sub_entries.flatten() {
if sub_entry.path().is_dir() {
let icon_path =
sub_entry.path().join(format!("{}{}", name, ext));
if icon_path.exists() {
log::debug!("Icon found (deep 2): {:?}", icon_path);
return Some(
icon_path.to_string_lossy().into_owned(),
);
}
}
}
}
}
}
}
}
}
}
None
}
fn load_icon(&self, identifier: &str) -> Option<ImageSurface> { fn load_icon(&self, identifier: &str) -> Option<ImageSurface> {
let path = if identifier.starts_with('/') { let path = if identifier.starts_with('~') {
std::path::PathBuf::from(identifier) let home = std::env::var("HOME").unwrap_or_default();
std::path::PathBuf::from(identifier.replacen('~', &home, 1))
} else { } else {
return None; std::path::PathBuf::from(identifier)
}; };
if let Ok(pixbuf) = gdk_pixbuf::Pixbuf::from_file(&path) { if !path.exists() {
log::warn!("Icon path does not exist: {:?}", path);
return None;
}
if path.extension().and_then(|s| s.to_str()) == Some("svg") {
match self.load_svg_with_resvg(&path) {
Some(surface) => return Some(surface),
None => log::warn!("resvg failed to load {:?}, falling back to pixbuf", path),
}
}
match gdk_pixbuf::Pixbuf::from_file(&path) {
Ok(pixbuf) => {
let w = pixbuf.width(); let w = pixbuf.width();
let h = pixbuf.height(); let h = pixbuf.height();
let mut surface = ImageSurface::create(Format::ARgb32, w, h).ok()?; let mut surface = ImageSurface::create(Format::ARgb32, w, h).ok()?;
@@ -120,6 +379,14 @@ impl Renderer {
let n_channels = pixbuf.n_channels(); let n_channels = pixbuf.n_channels();
let rowstride = pixbuf.rowstride() as usize; let rowstride = pixbuf.rowstride() as usize;
log::debug!(
"Loaded pixbuf from {:?}: {}x{}, {} channels",
path,
w,
h,
n_channels
);
for y in 0..h as usize { for y in 0..h as usize {
for x in 0..w as usize { for x in 0..w as usize {
let pix_idx = y * rowstride + x * n_channels as usize; let pix_idx = y * rowstride + x * n_channels as usize;
@@ -140,10 +407,50 @@ impl Renderer {
} }
} }
Some(surface) Some(surface)
} else { }
Err(e) => {
log::error!("Failed to load icon from {:?}: {}", path, e);
None None
} }
} }
}
fn load_svg_with_resvg(&self, path: &std::path::Path) -> Option<ImageSurface> {
use resvg::usvg;
let opt = usvg::Options::default();
let svg_data = std::fs::read(path).ok()?;
let tree = usvg::Tree::from_data(&svg_data, &opt).ok()?;
let size = tree.size().to_int_size();
let mut pixmap = resvg::tiny_skia::Pixmap::new(size.width(), size.height())?;
resvg::render(
&tree,
resvg::tiny_skia::Transform::default(),
&mut pixmap.as_mut(),
);
let mut surface =
ImageSurface::create(Format::ARgb32, size.width() as i32, size.height() as i32).ok()?;
{
let mut surface_data = surface.data().ok()?;
let pix_data = pixmap.data();
for i in (0..pix_data.len()).step_by(4) {
let r = pix_data[i];
let g = pix_data[i + 1];
let b = pix_data[i + 2];
let a = pix_data[i + 3];
surface_data[i] = b;
surface_data[i + 1] = g;
surface_data[i + 2] = r;
surface_data[i + 3] = a;
}
}
Some(surface)
}
/// Resize the renderer to new dimensions /// Resize the renderer to new dimensions
pub fn resize(&mut self, width: i32, height: i32) { pub fn resize(&mut self, width: i32, height: i32) {
@@ -199,6 +506,7 @@ impl Renderer {
/// Render the current frame /// Render the current frame
pub fn render(&mut self) { pub fn render(&mut self) {
self.media_rects.clear();
// Clear the surface // Clear the surface
self.context.new_path(); self.context.new_path();
self.context.set_source_rgba(0.0, 0.0, 0.0, 1.0); self.context.set_source_rgba(0.0, 0.0, 0.0, 1.0);
@@ -652,13 +960,17 @@ impl Renderer {
let status_text = if self.system_status.media_playing { let status_text = if self.system_status.media_playing {
if let Some(ref icon) = self.media_play_icon_surface { if let Some(ref icon) = self.media_play_icon_surface {
// Draw play icon instead of text
let play_y = start_y + 40.0; let play_y = start_y + 40.0;
self.draw_icon_at( let rx = center_x - icon.width() as f64 / 2.0;
center_x - icon.width() as f64 / 2.0, let ry = play_y - icon.height() as f64 / 2.0;
play_y - icon.height() as f64 / 2.0, self.draw_icon_at(rx, ry, icon);
icon, self.media_rects.push((
); "play".to_string(),
rx,
ry,
icon.width() as f64,
icon.height() as f64,
));
"" ""
} else { } else {
"▶ Playing" "▶ Playing"
@@ -685,7 +997,11 @@ impl Renderer {
// Previous icon // Previous icon
if let Some(ref icon) = self.media_prev_icon_surface { if let Some(ref icon) = self.media_prev_icon_surface {
let ix = controls_center_x - icon_spacing; let ix = controls_center_x - icon_spacing;
self.draw_icon_at(ix - icon_size / 2.0, controls_y - icon_size / 2.0, icon); let rx = ix - icon_size / 2.0;
let ry = controls_y - icon_size / 2.0;
self.draw_icon_at(rx, ry, icon);
self.media_rects
.push(("prev".to_string(), rx, ry, icon_size, icon_size));
} else { } else {
self.context self.context
.set_source_rgba(1.0, 1.0, 1.0, self.fade_alpha * 0.7); .set_source_rgba(1.0, 1.0, 1.0, self.fade_alpha * 0.7);
@@ -698,7 +1014,11 @@ impl Renderer {
// Stop icon // Stop icon
if let Some(ref icon) = self.media_stop_icon_surface { if let Some(ref icon) = self.media_stop_icon_surface {
let ix = controls_center_x; let ix = controls_center_x;
self.draw_icon_at(ix - icon_size / 2.0, controls_y - icon_size / 2.0, icon); let rx = ix - icon_size / 2.0;
let ry = controls_y - icon_size / 2.0;
self.draw_icon_at(rx, ry, icon);
self.media_rects
.push(("stop".to_string(), rx, ry, icon_size, icon_size));
} else { } else {
self.context self.context
.set_source_rgba(1.0, 1.0, 1.0, self.fade_alpha * 0.7); .set_source_rgba(1.0, 1.0, 1.0, self.fade_alpha * 0.7);
@@ -710,7 +1030,11 @@ impl Renderer {
// Next icon // Next icon
if let Some(ref icon) = self.media_next_icon_surface { if let Some(ref icon) = self.media_next_icon_surface {
let ix = controls_center_x + icon_spacing; let ix = controls_center_x + icon_spacing;
self.draw_icon_at(ix - icon_size / 2.0, controls_y - icon_size / 2.0, icon); let rx = ix - icon_size / 2.0;
let ry = controls_y - icon_size / 2.0;
self.draw_icon_at(rx, ry, icon);
self.media_rects
.push(("next".to_string(), rx, ry, icon_size, icon_size));
} else { } else {
self.context self.context
.set_source_rgba(1.0, 1.0, 1.0, self.fade_alpha * 0.7); .set_source_rgba(1.0, 1.0, 1.0, self.fade_alpha * 0.7);
@@ -733,7 +1057,7 @@ impl Renderer {
if let Some(ref ssid) = self.system_status.wifi_ssid { if let Some(ref ssid) = self.system_status.wifi_ssid {
if let Some(ref icon) = self.wifi_icon_surface { if let Some(ref icon) = self.wifi_icon_surface {
self.draw_icon_at(x, y - 15.0, icon); self.draw_icon_at(x, y - 15.0, icon);
let text_x = x + icon.width() as f64 + 10.0; let text_x = x + 24.0 + 10.0;
self.context.new_path(); self.context.new_path();
self.context.set_source_rgba(1.0, 1.0, 1.0, self.fade_alpha); self.context.set_source_rgba(1.0, 1.0, 1.0, self.fade_alpha);
self.context.set_font_size(16.0); self.context.set_font_size(16.0);
@@ -766,7 +1090,7 @@ impl Renderer {
if let Some(ref icon) = self.battery_icon_surface { if let Some(ref icon) = self.battery_icon_surface {
self.draw_icon_at(x, y - 15.0, icon); self.draw_icon_at(x, y - 15.0, icon);
let text_x = x + icon.width() as f64 + 10.0; let text_x = x + 24.0 + 10.0;
let battery_text = format!("{:.0}%", percent); let battery_text = format!("{:.0}%", percent);
self.context.new_path(); self.context.new_path();
self.context.set_source_rgba(1.0, 1.0, 1.0, self.fade_alpha); self.context.set_source_rgba(1.0, 1.0, 1.0, self.fade_alpha);
@@ -800,35 +1124,37 @@ impl Renderer {
let x = margin; let x = margin;
let y = margin + 50.0; let y = margin + 50.0;
let status_text;
let is_off;
if self.system_status.bluetooth_connected { if self.system_status.bluetooth_connected {
if let Some(ref icon) = self.bluetooth_icon_surface { status_text = self.system_status.bluetooth_devices.join(", ");
self.draw_icon_at(x, y - 12.0, icon); is_off = false;
let text_x = x + icon.width() as f64 + 10.0;
let devices = self.system_status.bluetooth_devices.join(", ");
self.context.new_path();
self.context.set_source_rgba(1.0, 1.0, 1.0, self.fade_alpha);
self.context.set_font_size(14.0);
self.context.move_to(text_x, y);
self.context.show_text(&devices).unwrap();
} else { } else {
let text = format!( status_text = "Bluetooth off".to_string();
"🔵 {} device(s)", is_off = true;
self.system_status.bluetooth_devices.len()
);
self.context.new_path();
self.context.set_source_rgba(1.0, 1.0, 1.0, self.fade_alpha);
self.context.set_font_size(14.0);
self.context.move_to(x, y);
self.context.show_text(&text).unwrap();
} }
} else {
let text = "🔴 Bluetooth off"; let alpha_mult = if is_off { 0.5 } else { 1.0 };
if let Some(ref icon) = self.bluetooth_icon_surface {
self.draw_icon_at_with_alpha(x, y - 12.0, icon, alpha_mult);
let text_x = x + 24.0 + 10.0;
self.context.new_path(); self.context.new_path();
self.context self.context
.set_source_rgba(1.0, 1.0, 1.0, self.fade_alpha * 0.5); .set_source_rgba(1.0, 1.0, 1.0, self.fade_alpha * alpha_mult);
self.context.set_font_size(14.0);
self.context.move_to(text_x, y);
self.context.show_text(&status_text).unwrap();
} else {
let emoji = if is_off { "🔴 " } else { "🔵 " };
let full_text = format!("{}{}", emoji, status_text);
self.context.new_path();
self.context
.set_source_rgba(1.0, 1.0, 1.0, self.fade_alpha * alpha_mult);
self.context.set_font_size(14.0); self.context.set_font_size(14.0);
self.context.move_to(x, y); self.context.move_to(x, y);
self.context.show_text(text).unwrap(); self.context.show_text(&full_text).unwrap();
} }
} }
@@ -862,6 +1188,21 @@ impl Renderer {
self.context.restore().unwrap(); self.context.restore().unwrap();
} }
fn draw_icon_at_with_alpha(&self, x: f64, y: f64, surface: &ImageSurface, alpha: f64) {
self.context.save().unwrap();
let target_size = 24.0;
let scale_x = target_size / surface.width() as f64;
let scale_y = target_size / surface.height() as f64;
let scale = scale_x.min(scale_y);
self.context.translate(x, y);
self.context.scale(scale, scale);
self.context.set_source_surface(surface, 0.0, 0.0).unwrap();
self.context
.paint_with_alpha(self.fade_alpha * alpha)
.unwrap();
self.context.restore().unwrap();
}
fn draw_battery_icon_at( fn draw_battery_icon_at(
&self, &self,
x: f64, x: f64,
+8
View File
@@ -259,6 +259,14 @@ impl SystemManager {
} }
} }
pub fn media_stop(&self) {
if let Ok(finder) = PlayerFinder::new() {
if let Ok(player) = finder.find_active() {
let _ = player.stop();
}
}
}
pub fn media_next(&self) { pub fn media_next(&self) {
if let Ok(finder) = PlayerFinder::new() { if let Ok(finder) = PlayerFinder::new() {
if let Ok(player) = finder.find_active() { if let Ok(player) = finder.find_active() {