//! Set the native app icon at runtime. //! //! TODO(emilk): port this to [`winit`]. use std::sync::Arc; use egui::IconData; pub struct AppTitleIconSetter { title: String, icon_data: Option>, status: AppIconStatus, } impl AppTitleIconSetter { pub fn new(title: String, mut icon_data: Option>) -> Self { if let Some(icon) = &icon_data { if **icon == IconData::default() { icon_data = None; } } Self { title, icon_data, status: AppIconStatus::NotSetTryAgain, } } /// Call once per frame; we will set the icon when we can. pub fn update(&mut self) { if self.status == AppIconStatus::NotSetTryAgain { self.status = set_title_and_icon(&self.title, self.icon_data.as_deref()); } } } /// In which state the app icon is (as far as we know). #[derive(PartialEq, Eq)] enum AppIconStatus { /// We did not set it or failed to do it. In any case we won't try again. NotSetIgnored, /// We haven't set the icon yet, we should try again next frame. /// /// This can happen repeatedly due to lazy window creation on some platforms. NotSetTryAgain, /// We successfully set the icon and it should be visible now. #[allow(dead_code)] // Not used on Linux Set, } /// Sets app icon at runtime. /// /// By setting the icon at runtime and not via resource files etc. we ensure that we'll get the chance /// to set the same icon when the process/window is started from python (which sets its own icon ahead of us!). /// /// Since window creation can be lazy, call this every frame until it's either successfully or gave up. /// (See [`AppIconStatus`]) fn set_title_and_icon(_title: &str, _icon_data: Option<&IconData>) -> AppIconStatus { crate::profile_function!(); #[cfg(target_os = "windows")] { if let Some(icon_data) = _icon_data { return set_app_icon_windows(icon_data); } } #[cfg(target_os = "macos")] return set_title_and_icon_mac(_title, _icon_data); #[allow(unreachable_code)] AppIconStatus::NotSetIgnored } /// Set icon for Windows applications. #[cfg(target_os = "windows")] #[allow(unsafe_code)] fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus { use crate::icon_data::IconDataExt as _; use winapi::um::winuser; // We would get fairly far already with winit's `set_window_icon` (which is exposed to eframe) actually! // However, it only sets ICON_SMALL, i.e. doesn't allow us to set a higher resolution icon for the task bar. // Also, there is scaling issues, detailed below. // TODO(andreas): This does not set the task bar icon for when our application is started from python. // Things tried so far: // * Querying for an owning window and setting icon there (there doesn't seem to be an owning window) // * using undocumented SetConsoleIcon method (successfully queried via GetProcAddress) // SAFETY: WinApi function without side-effects. let window_handle = unsafe { winuser::GetActiveWindow() }; if window_handle.is_null() { // The Window isn't available yet. Try again later! return AppIconStatus::NotSetTryAgain; } fn create_hicon_with_scale( unscaled_image: &image::RgbaImage, target_size: i32, ) -> winapi::shared::windef::HICON { let image_scaled = image::imageops::resize( unscaled_image, target_size as _, target_size as _, image::imageops::Lanczos3, ); // Creating transparent icons with WinApi is a huge mess. // We'd need to go through CreateIconIndirect's ICONINFO struct which then // takes a mask HBITMAP and a color HBITMAP and creating each of these is pain. // Instead we workaround this by creating a png which CreateIconFromResourceEx magically understands. // This is a pretty horrible hack as we spend a lot of time encoding, but at least the code is a lot shorter. let mut image_scaled_bytes: Vec = Vec::new(); if image_scaled .write_to( &mut std::io::Cursor::new(&mut image_scaled_bytes), image::ImageOutputFormat::Png, ) .is_err() { return std::ptr::null_mut(); } // SAFETY: Creating an HICON which should be readonly on our data. unsafe { winuser::CreateIconFromResourceEx( image_scaled_bytes.as_mut_ptr(), image_scaled_bytes.len() as u32, 1, // Means this is an icon, not a cursor. 0x00030000, // Version number of the HICON target_size, // Note that this method can scale, but it does so *very* poorly. So let's avoid that! target_size, winuser::LR_DEFAULTCOLOR, ) } } let unscaled_image = match icon_data.to_image() { Ok(unscaled_image) => unscaled_image, Err(err) => { log::warn!("Invalid icon: {err}"); return AppIconStatus::NotSetIgnored; } }; // Only setting ICON_BIG with the icon size for big icons (SM_CXICON) works fine // but the scaling it does then for the small icon is pretty bad. // Instead we set the correct sizes manually and take over the scaling ourselves. // For this to work we first need to set the big icon and then the small one. // // Note that ICON_SMALL may be used even if we don't render a title bar as it may be used in alt+tab! { // SAFETY: WinAPI getter function with no known side effects. let icon_size_big = unsafe { winuser::GetSystemMetrics(winuser::SM_CXICON) }; let icon_big = create_hicon_with_scale(&unscaled_image, icon_size_big); if icon_big.is_null() { log::warn!("Failed to create HICON (for big icon) from embedded png data."); return AppIconStatus::NotSetIgnored; // We could try independently with the small icon but what's the point, it would look bad! } else { // SAFETY: Unsafe WinApi function, takes objects previously created with WinAPI, all checked for null prior. unsafe { winuser::SendMessageW( window_handle, winuser::WM_SETICON, winuser::ICON_BIG as usize, icon_big as isize, ); } } } { // SAFETY: WinAPI getter function with no known side effects. let icon_size_small = unsafe { winuser::GetSystemMetrics(winuser::SM_CXSMICON) }; let icon_small = create_hicon_with_scale(&unscaled_image, icon_size_small); if icon_small.is_null() { log::warn!("Failed to create HICON (for small icon) from embedded png data."); return AppIconStatus::NotSetIgnored; } else { // SAFETY: Unsafe WinApi function, takes objects previously created with WinAPI, all checked for null prior. unsafe { winuser::SendMessageW( window_handle, winuser::WM_SETICON, winuser::ICON_SMALL as usize, icon_small as isize, ); } } } // It _probably_ worked out. AppIconStatus::Set } /// Set icon & app title for `MacOS` applications. #[cfg(target_os = "macos")] #[allow(unsafe_code)] fn set_title_and_icon_mac(title: &str, icon_data: Option<&IconData>) -> AppIconStatus { use crate::icon_data::IconDataExt as _; crate::profile_function!(); use objc2::ClassType; use objc2_app_kit::{NSApplication, NSImage}; use objc2_foundation::{NSData, NSString}; let png_bytes = if let Some(icon_data) = icon_data { match icon_data.to_png_bytes() { Ok(png_bytes) => Some(png_bytes), Err(err) => { log::warn!("Failed to convert IconData to png: {err}"); return AppIconStatus::NotSetIgnored; } } } else { None }; // TODO(madsmtm): Move this into `objc2-app-kit` extern "C" { static NSApp: Option<&'static NSApplication>; } unsafe { let Some(app) = NSApp else { log::debug!("NSApp is null"); return AppIconStatus::NotSetIgnored; }; if let Some(png_bytes) = png_bytes { let data = NSData::from_vec(png_bytes); log::trace!("NSImage::initWithData…"); let app_icon = NSImage::initWithData(NSImage::alloc(), &data); crate::profile_scope!("setApplicationIconImage_"); log::trace!("setApplicationIconImage…"); app.setApplicationIconImage(app_icon.as_deref()); } // Change the title in the top bar - for python processes this would be again "python" otherwise. if let Some(main_menu) = app.mainMenu() { if let Some(item) = main_menu.itemAtIndex(0) { if let Some(app_menu) = item.submenu() { crate::profile_scope!("setTitle_"); app_menu.setTitle(&NSString::from_str(title)); } } } // The title in the Dock apparently can't be changed. // At least these people didn't figure it out either: // https://stackoverflow.com/questions/69831167/qt-change-application-title-dynamically-on-macos // https://stackoverflow.com/questions/28808226/changing-cocoa-app-icon-title-and-menu-labels-at-runtime } AppIconStatus::Set }