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
}