eframe/native/app_icon.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262
//! 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<Arc<IconData>>,
status: AppIconStatus,
}
impl AppTitleIconSetter {
pub fn new(title: String, mut icon_data: Option<Arc<IconData>>) -> 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 {
profiling::function_scope!();
#[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<u8> = Vec::new();
if image_scaled
.write_to(
&mut std::io::Cursor::new(&mut image_scaled_bytes),
image::ImageFormat::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 _;
profiling::function_scope!();
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>;
}
// SAFETY: we don't do anything dangerous here
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);
profiling::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() {
profiling::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
}