From e26159f85f3142e2df0c9ff1329527e56b7e7503 Mon Sep 17 00:00:00 2001 From: Ultradesu Date: Wed, 12 Aug 2026 01:56:21 +0100 Subject: [PATCH] Added app icon --- Cargo.lock | 3 +++ Cargo.toml | 7 ++++++ crates/platform-desktop/Cargo.toml | 3 +++ crates/platform-desktop/src/app.rs | 38 ++++++++++++++++++++++++++++++ crates/platform-desktop/src/lib.rs | 2 ++ crates/ui/src/lib.rs | 7 ++++++ crates/ui/ui/app.slint | 1 + 7 files changed, 61 insertions(+) create mode 100644 crates/platform-desktop/src/app.rs diff --git a/Cargo.lock b/Cargo.lock index 72ea895..0be4f57 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2175,6 +2175,9 @@ name = "furumi-platform-desktop" version = "0.1.0" dependencies = [ "core-foundation 0.10.1", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-foundation 0.3.2", "rfd", "souvlaki", "windows-sys 0.61.2", diff --git a/Cargo.toml b/Cargo.toml index 1df8ac5..308ccbe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,13 @@ rodio = { version = "0.22.2", default-features = false, features = [ ] } souvlaki = { version = "0.8.3", default-features = false, features = ["use_zbus"] } core-foundation = "0.10.1" +objc2 = "0.6.4" +objc2-app-kit = { version = "0.3.2", default-features = false, features = [ + "NSApplication", + "NSImage", + "NSResponder", +] } +objc2-foundation = { version = "0.3.2", default-features = false, features = ["NSData"] } windows-sys = { version = "0.61.2", features = ["Win32_System_LibraryLoader", "Win32_UI_WindowsAndMessaging"] } furumi-application = { path = "crates/application" } diff --git a/crates/platform-desktop/Cargo.toml b/crates/platform-desktop/Cargo.toml index 2d2616c..ca309f0 100644 --- a/crates/platform-desktop/Cargo.toml +++ b/crates/platform-desktop/Cargo.toml @@ -11,6 +11,9 @@ souvlaki.workspace = true [target.'cfg(target_os="macos")'.dependencies] core-foundation.workspace = true +objc2.workspace = true +objc2-app-kit.workspace = true +objc2-foundation.workspace = true [target.'cfg(windows)'.dependencies] windows-sys.workspace = true diff --git a/crates/platform-desktop/src/app.rs b/crates/platform-desktop/src/app.rs new file mode 100644 index 0000000..90e13f5 --- /dev/null +++ b/crates/platform-desktop/src/app.rs @@ -0,0 +1,38 @@ +/// Applies the Furumi federation mark as the native application icon. +/// +/// Slint forwards its window icon to Windows and X11. macOS deliberately +/// ignores that window-level API, so its Dock icon is installed through +/// `AppKit` from the same embedded SVG asset. On macOS this must be called +/// from the main event loop after `applicationDidFinishLaunching`. +pub fn set_application_icon() { + if let Err(error) = set_native_application_icon() { + eprintln!("failed to install the Furumi application icon: {error}"); + } +} + +#[cfg(target_os = "macos")] +fn set_native_application_icon() -> Result<(), &'static str> { + use std::ffi::c_void; + + use objc2::MainThreadMarker; + use objc2_app_kit::{NSApplication, NSImage}; + use objc2_foundation::NSData; + + let main_thread = MainThreadMarker::new().ok_or("not running on the macOS main thread")?; + let svg = include_bytes!("../../ui/assets/federation.svg"); + // SAFETY: `dataWithBytes_length` copies `svg`; its pointer is valid for + // the complete duration of the Objective-C call. + let data = unsafe { NSData::dataWithBytes_length(svg.as_ptr().cast::(), svg.len()) }; + let icon = NSImage::initWithData(main_thread.alloc(), &data) + .ok_or("AppKit could not decode the embedded federation SVG")?; + let application = NSApplication::sharedApplication(main_thread); + // SAFETY: AppKit accepts a live NSImage here and retains it as the + // application's Dock icon. This function is restricted to the main thread. + unsafe { application.setApplicationIconImage(Some(&icon)) }; + Ok(()) +} + +#[cfg(not(target_os = "macos"))] +fn set_native_application_icon() -> Result<(), &'static str> { + Ok(()) +} diff --git a/crates/platform-desktop/src/lib.rs b/crates/platform-desktop/src/lib.rs index b7a86bc..170d694 100644 --- a/crates/platform-desktop/src/lib.rs +++ b/crates/platform-desktop/src/lib.rs @@ -2,7 +2,9 @@ use std::path::{Path, PathBuf}; +mod app; mod media; +pub use app::set_application_icon; pub use media::{MediaCommand, MediaSession}; /// Opens the platform-native directory picker. diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs index c7cb6ea..593ca93 100644 --- a/crates/ui/src/lib.rs +++ b/crates/ui/src/lib.rs @@ -50,6 +50,13 @@ pub fn run(backend: &BackendHandle) -> Result<(), slint::PlatformError> { let window = AppWindow::new()?; let state = Arc::new(Mutex::new(AppState::default())); + // Winit completes NSApplication initialization only after entering the + // event loop. AppKit discards a Dock icon assigned before that point. + #[cfg(target_os = "macos")] + slint::Timer::single_shot(std::time::Duration::ZERO, || { + furumi_platform_desktop::set_application_icon(); + }); + let media_backend = backend.clone(); MEDIA_SESSION.with_borrow_mut(|session| { *session = MediaSession::new(move |command| { diff --git a/crates/ui/ui/app.slint b/crates/ui/ui/app.slint index 74c348b..03d3a5e 100644 --- a/crates/ui/ui/app.slint +++ b/crates/ui/ui/app.slint @@ -4,6 +4,7 @@ import { AlbumGrid, ArtistGrid, ArtistLinksRow, BuildInfoCard, DeviceRow, Federa export component AppWindow inherits Window { title: "Furumi Desktop"; + icon: @image-url("../assets/federation.svg"); preferred-width: 1180px; preferred-height: 760px; min-width: 920px;