eframe/native/
app_icon.rs

1//! Set the native app icon at runtime.
2//!
3//! TODO(emilk): port this to [`winit`].
4
5use std::sync::Arc;
6
7use egui::IconData;
8
9pub struct AppTitleIconSetter {
10    title: String,
11    icon_data: Option<Arc<IconData>>,
12    status: AppIconStatus,
13}
14
15impl AppTitleIconSetter {
16    pub fn new(title: String, mut icon_data: Option<Arc<IconData>>) -> Self {
17        if let Some(icon) = &icon_data {
18            if **icon == IconData::default() {
19                icon_data = None;
20            }
21        }
22
23        Self {
24            title,
25            icon_data,
26            status: AppIconStatus::NotSetTryAgain,
27        }
28    }
29
30    /// Call once per frame; we will set the icon when we can.
31    pub fn update(&mut self) {
32        if self.status == AppIconStatus::NotSetTryAgain {
33            self.status = set_title_and_icon(&self.title, self.icon_data.as_deref());
34        }
35    }
36}
37
38/// In which state the app icon is (as far as we know).
39#[derive(PartialEq, Eq)]
40enum AppIconStatus {
41    /// We did not set it or failed to do it. In any case we won't try again.
42    NotSetIgnored,
43
44    /// We haven't set the icon yet, we should try again next frame.
45    ///
46    /// This can happen repeatedly due to lazy window creation on some platforms.
47    NotSetTryAgain,
48
49    /// We successfully set the icon and it should be visible now.
50    #[allow(dead_code)] // Not used on Linux
51    Set,
52}
53
54/// Sets app icon at runtime.
55///
56/// By setting the icon at runtime and not via resource files etc. we ensure that we'll get the chance
57/// to set the same icon when the process/window is started from python (which sets its own icon ahead of us!).
58///
59/// Since window creation can be lazy, call this every frame until it's either successfully or gave up.
60/// (See [`AppIconStatus`])
61fn set_title_and_icon(_title: &str, _icon_data: Option<&IconData>) -> AppIconStatus {
62    profiling::function_scope!();
63
64    #[cfg(target_os = "windows")]
65    {
66        if let Some(icon_data) = _icon_data {
67            return set_app_icon_windows(icon_data);
68        }
69    }
70
71    #[cfg(target_os = "macos")]
72    return set_title_and_icon_mac(_title, _icon_data);
73
74    #[allow(unreachable_code)]
75    AppIconStatus::NotSetIgnored
76}
77
78/// Set icon for Windows applications.
79#[cfg(target_os = "windows")]
80#[allow(unsafe_code)]
81fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus {
82    use crate::icon_data::IconDataExt as _;
83    use winapi::um::winuser;
84
85    // We would get fairly far already with winit's `set_window_icon` (which is exposed to eframe) actually!
86    // However, it only sets ICON_SMALL, i.e. doesn't allow us to set a higher resolution icon for the task bar.
87    // Also, there is scaling issues, detailed below.
88
89    // TODO(andreas): This does not set the task bar icon for when our application is started from python.
90    //      Things tried so far:
91    //      * Querying for an owning window and setting icon there (there doesn't seem to be an owning window)
92    //      * using undocumented SetConsoleIcon method (successfully queried via GetProcAddress)
93
94    // SAFETY: WinApi function without side-effects.
95    let window_handle = unsafe { winuser::GetActiveWindow() };
96    if window_handle.is_null() {
97        // The Window isn't available yet. Try again later!
98        return AppIconStatus::NotSetTryAgain;
99    }
100
101    fn create_hicon_with_scale(
102        unscaled_image: &image::RgbaImage,
103        target_size: i32,
104    ) -> winapi::shared::windef::HICON {
105        let image_scaled = image::imageops::resize(
106            unscaled_image,
107            target_size as _,
108            target_size as _,
109            image::imageops::Lanczos3,
110        );
111
112        // Creating transparent icons with WinApi is a huge mess.
113        // We'd need to go through CreateIconIndirect's ICONINFO struct which then
114        // takes a mask HBITMAP and a color HBITMAP and creating each of these is pain.
115        // Instead we workaround this by creating a png which CreateIconFromResourceEx magically understands.
116        // This is a pretty horrible hack as we spend a lot of time encoding, but at least the code is a lot shorter.
117        let mut image_scaled_bytes: Vec<u8> = Vec::new();
118        if image_scaled
119            .write_to(
120                &mut std::io::Cursor::new(&mut image_scaled_bytes),
121                image::ImageFormat::Png,
122            )
123            .is_err()
124        {
125            return std::ptr::null_mut();
126        }
127
128        // SAFETY: Creating an HICON which should be readonly on our data.
129        unsafe {
130            winuser::CreateIconFromResourceEx(
131                image_scaled_bytes.as_mut_ptr(),
132                image_scaled_bytes.len() as u32,
133                1,           // Means this is an icon, not a cursor.
134                0x00030000,  // Version number of the HICON
135                target_size, // Note that this method can scale, but it does so *very* poorly. So let's avoid that!
136                target_size,
137                winuser::LR_DEFAULTCOLOR,
138            )
139        }
140    }
141
142    let unscaled_image = match icon_data.to_image() {
143        Ok(unscaled_image) => unscaled_image,
144        Err(err) => {
145            log::warn!("Invalid icon: {err}");
146            return AppIconStatus::NotSetIgnored;
147        }
148    };
149
150    // Only setting ICON_BIG with the icon size for big icons (SM_CXICON) works fine
151    // but the scaling it does then for the small icon is pretty bad.
152    // Instead we set the correct sizes manually and take over the scaling ourselves.
153    // For this to work we first need to set the big icon and then the small one.
154    //
155    // Note that ICON_SMALL may be used even if we don't render a title bar as it may be used in alt+tab!
156    {
157        // SAFETY: WinAPI getter function with no known side effects.
158        let icon_size_big = unsafe { winuser::GetSystemMetrics(winuser::SM_CXICON) };
159        let icon_big = create_hicon_with_scale(&unscaled_image, icon_size_big);
160        if icon_big.is_null() {
161            log::warn!("Failed to create HICON (for big icon) from embedded png data.");
162            return AppIconStatus::NotSetIgnored; // We could try independently with the small icon but what's the point, it would look bad!
163        } else {
164            // SAFETY: Unsafe WinApi function, takes objects previously created with WinAPI, all checked for null prior.
165            unsafe {
166                winuser::SendMessageW(
167                    window_handle,
168                    winuser::WM_SETICON,
169                    winuser::ICON_BIG as usize,
170                    icon_big as isize,
171                );
172            }
173        }
174    }
175    {
176        // SAFETY: WinAPI getter function with no known side effects.
177        let icon_size_small = unsafe { winuser::GetSystemMetrics(winuser::SM_CXSMICON) };
178        let icon_small = create_hicon_with_scale(&unscaled_image, icon_size_small);
179        if icon_small.is_null() {
180            log::warn!("Failed to create HICON (for small icon) from embedded png data.");
181            return AppIconStatus::NotSetIgnored;
182        } else {
183            // SAFETY: Unsafe WinApi function, takes objects previously created with WinAPI, all checked for null prior.
184            unsafe {
185                winuser::SendMessageW(
186                    window_handle,
187                    winuser::WM_SETICON,
188                    winuser::ICON_SMALL as usize,
189                    icon_small as isize,
190                );
191            }
192        }
193    }
194
195    // It _probably_ worked out.
196    AppIconStatus::Set
197}
198
199/// Set icon & app title for `MacOS` applications.
200#[cfg(target_os = "macos")]
201#[allow(unsafe_code)]
202fn set_title_and_icon_mac(title: &str, icon_data: Option<&IconData>) -> AppIconStatus {
203    use crate::icon_data::IconDataExt as _;
204    profiling::function_scope!();
205
206    use objc2::ClassType;
207    use objc2_app_kit::{NSApplication, NSImage};
208    use objc2_foundation::{NSData, NSString};
209
210    let png_bytes = if let Some(icon_data) = icon_data {
211        match icon_data.to_png_bytes() {
212            Ok(png_bytes) => Some(png_bytes),
213            Err(err) => {
214                log::warn!("Failed to convert IconData to png: {err}");
215                return AppIconStatus::NotSetIgnored;
216            }
217        }
218    } else {
219        None
220    };
221
222    // TODO(madsmtm): Move this into `objc2-app-kit`
223    extern "C" {
224        static NSApp: Option<&'static NSApplication>;
225    }
226
227    // SAFETY: we don't do anything dangerous here
228    unsafe {
229        let Some(app) = NSApp else {
230            log::debug!("NSApp is null");
231            return AppIconStatus::NotSetIgnored;
232        };
233
234        if let Some(png_bytes) = png_bytes {
235            let data = NSData::from_vec(png_bytes);
236
237            log::trace!("NSImage::initWithData…");
238            let app_icon = NSImage::initWithData(NSImage::alloc(), &data);
239
240            profiling::scope!("setApplicationIconImage_");
241            log::trace!("setApplicationIconImage…");
242            app.setApplicationIconImage(app_icon.as_deref());
243        }
244
245        // Change the title in the top bar - for python processes this would be again "python" otherwise.
246        if let Some(main_menu) = app.mainMenu() {
247            if let Some(item) = main_menu.itemAtIndex(0) {
248                if let Some(app_menu) = item.submenu() {
249                    profiling::scope!("setTitle_");
250                    app_menu.setTitle(&NSString::from_str(title));
251                }
252            }
253        }
254
255        // The title in the Dock apparently can't be changed.
256        // At least these people didn't figure it out either:
257        // https://stackoverflow.com/questions/69831167/qt-change-application-title-dynamically-on-macos
258        // https://stackoverflow.com/questions/28808226/changing-cocoa-app-icon-title-and-menu-labels-at-runtime
259    }
260
261    AppIconStatus::Set
262}