Browse Source

eframe: Set app icon on Mac and Windows (#2940)

* eframe: Set app icon on Mac and Windows

Also: correctly set window title on Mac when launching from
another process, e.g. python.

Co-authored-by: Wumpf <andreas@rerun.io>

* lint fixes

* Fix web build

* fix typo

* Try fix windows build

---------

Co-authored-by: Wumpf <andreas@rerun.io>
pull/2948/head
Emil Ernerfeldt 2 years ago
committed by GitHub
parent
commit
7f2de426d2
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 19
      Cargo.lock
  2. 17
      crates/eframe/Cargo.toml
  3. 64
      crates/eframe/src/epi/icon_data.rs
  4. 19
      crates/eframe/src/epi/mod.rs
  5. 239
      crates/eframe/src/native/app_icon.rs
  6. 14
      crates/eframe/src/native/epi_integration.rs
  7. 1
      crates/eframe/src/native/mod.rs
  8. 6
      crates/eframe/src/native/run.rs
  9. 5
      crates/egui_demo_app/src/main.rs
  10. BIN
      media/icon.png

19
Cargo.lock

@ -697,6 +697,22 @@ dependencies = [
"winapi",
]
[[package]]
name = "cocoa"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f425db7937052c684daec3bd6375c8abe2d146dca4b8b143d6db777c39138f3a"
dependencies = [
"bitflags",
"block",
"cocoa-foundation",
"core-foundation",
"core-graphics",
"foreign-types",
"libc",
"objc",
]
[[package]]
name = "cocoa-foundation"
version = "0.1.0"
@ -1182,6 +1198,7 @@ name = "eframe"
version = "0.21.3"
dependencies = [
"bytemuck",
"cocoa",
"directories-next",
"document-features",
"egui",
@ -1194,6 +1211,7 @@ dependencies = [
"image",
"js-sys",
"log",
"objc",
"percent-encoding",
"pollster",
"puffin",
@ -1206,6 +1224,7 @@ dependencies = [
"wasm-bindgen-futures",
"web-sys",
"wgpu",
"winapi",
"winit",
]

17
crates/eframe/Cargo.toml

@ -58,7 +58,7 @@ web_screen_reader = ["tts"]
## If set, eframe will look for the env-var `EFRAME_SCREENSHOT_TO` and write a screenshot to that location, and then quit.
## This is used to generate images for the examples.
__screenshot = ["dep:image"]
__screenshot = []
## Use [`wgpu`](https://docs.rs/wgpu) for painting (via [`egui-wgpu`](https://github.com/emilk/egui/tree/master/crates/egui-wgpu)).
## This overrides the `glow` feature.
@ -98,6 +98,9 @@ egui-winit = { version = "0.21.1", path = "../egui-winit", default-features = fa
"clipboard",
"links",
] }
image = { version = "0.24", default-features = false, features = [
"png",
] } # Needed for app icon
raw-window-handle = { version = "0.5.0" }
winit = "0.28.1"
@ -112,12 +115,18 @@ pollster = { version = "0.3", optional = true } # needed for wgpu
# this can be done at the same time we expose x11/wayland features of winit crate.
glutin = { version = "0.30", optional = true }
glutin-winit = { version = "0.3.0", optional = true }
image = { version = "0.24", optional = true, default-features = false, features = [
"png",
] }
puffin = { version = "0.14", optional = true }
wgpu = { version = "0.15.0", optional = true }
# mac:
[target.'cfg(any(target_os = "macos"))'.dependencies]
cocoa = "0.24.1"
objc = "0.2.7"
# windows:
[target.'cfg(any(target_os = "windows"))'.dependencies]
winapi = "0.3.9"
# -------------------------------------------
# web:
[target.'cfg(target_arch = "wasm32")'.dependencies]

64
crates/eframe/src/epi/icon_data.rs

@ -0,0 +1,64 @@
/// Image data for an application icon.
///
/// Use a square image, e.g. 256x256 pixels.
/// You can use a transparent background.
#[derive(Clone)]
pub struct IconData {
/// RGBA pixels, with separate/unmultiplied alpha.
pub rgba: Vec<u8>,
/// Image width. This should be a multiple of 4.
pub width: u32,
/// Image height. This should be a multiple of 4.
pub height: u32,
}
impl IconData {
/// Convert into [`image::RgbaImage`]
///
/// # Errors
/// If this is not a valid png.
pub fn try_from_png_bytes(png_bytes: &[u8]) -> Result<Self, image::ImageError> {
let image = image::load_from_memory(png_bytes)?;
Ok(Self::from_image(image))
}
fn from_image(image: image::DynamicImage) -> Self {
let image = image.into_rgba8();
Self {
width: image.width(),
height: image.height(),
rgba: image.into_raw(),
}
}
/// Convert into [`image::RgbaImage`]
///
/// # Errors
/// If `width*height != 4 * rgba.len()`, or if the image is too big.
pub fn to_image(&self) -> Result<image::RgbaImage, String> {
let Self {
rgba,
width,
height,
} = self.clone();
image::RgbaImage::from_raw(width, height, rgba).ok_or_else(|| "Invalid IconData".to_owned())
}
/// Encode as PNG.
///
/// # Errors
/// The image is invalid, or the PNG encoder failed.
pub fn to_png_bytes(&self) -> Result<Vec<u8>, String> {
let image = self.to_image()?;
let mut png_bytes: Vec<u8> = Vec::new();
image
.write_to(
&mut std::io::Cursor::new(&mut png_bytes),
image::ImageOutputFormat::Png,
)
.map_err(|err| err.to_string())?;
Ok(png_bytes)
}
}

19
crates/eframe/src/epi.rs → crates/eframe/src/epi/mod.rs

@ -6,6 +6,12 @@
#![warn(missing_docs)] // Let's keep `epi` well-documented.
#[cfg(not(target_arch = "wasm32"))]
mod icon_data;
#[cfg(not(target_arch = "wasm32"))]
pub use icon_data::IconData;
#[cfg(target_arch = "wasm32")]
use std::any::Any;
@ -621,19 +627,6 @@ impl std::str::FromStr for Renderer {
// ----------------------------------------------------------------------------
/// Image data for an application icon.
#[derive(Clone)]
pub struct IconData {
/// RGBA pixels, unmultiplied.
pub rgba: Vec<u8>,
/// Image width. This should be a multiple of 4.
pub width: u32,
/// Image height. This should be a multiple of 4.
pub height: u32,
}
/// Represents the surroundings of your app.
///
/// It provides methods to inspect the surroundings (are we on the web?),

239
crates/eframe/src/native/app_icon.rs

@ -0,0 +1,239 @@
//! Set the native app icon at runtime.
//!
//! TODO(emilk): port this to [`winit`].
use crate::IconData;
pub struct AppTitleIconSetter {
title: String,
icon_data: Option<IconData>,
status: AppIconStatus,
}
impl AppTitleIconSetter {
pub fn new(title: String, icon_data: Option<IconData>) -> Self {
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_ref());
}
}
}
/// 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 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::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 cocoa::{
appkit::{NSApp, NSApplication, NSImage, NSMenu, NSWindow},
base::{id, nil},
foundation::{NSData, NSString},
};
use objc::{msg_send, sel, sel_impl};
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
};
// SAFETY: Accessing raw data from icon in a read-only manner. Icon data is static!
unsafe {
let app = NSApp();
if let Some(png_bytes) = png_bytes {
let data = NSData::dataWithBytes_length_(
nil,
png_bytes.as_ptr().cast::<std::ffi::c_void>(),
png_bytes.len() as u64,
);
let app_icon = NSImage::initWithData_(NSImage::alloc(nil), data);
app.setApplicationIconImage_(app_icon);
}
// Change the title in the top bar - for python processes this would be again "python" otherwise.
let main_menu = app.mainMenu();
let app_menu: id = msg_send![main_menu.itemAtIndex_(0), submenu];
app_menu.setTitle_(NSString::alloc(nil).init_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
}

14
crates/eframe/src/native/epi_integration.rs

@ -329,6 +329,7 @@ pub struct EpiIntegration {
can_drag_window: bool,
window_state: WindowState,
follow_system_theme: bool,
app_icon_setter: super::app_icon::AppTitleIconSetter,
}
impl EpiIntegration {
@ -338,7 +339,8 @@ impl EpiIntegration {
max_texture_side: usize,
window: &winit::window::Window,
system_theme: Option<Theme>,
follow_system_theme: bool,
app_name: &str,
native_options: &crate::NativeOptions,
storage: Option<Box<dyn epi::Storage>>,
#[cfg(feature = "glow")] gl: Option<std::sync::Arc<glow::Context>>,
#[cfg(feature = "wgpu")] wgpu_render_state: Option<egui_wgpu::RenderState>,
@ -378,6 +380,11 @@ impl EpiIntegration {
egui_winit.set_max_texture_side(max_texture_side);
egui_winit.set_pixels_per_point(native_pixels_per_point);
let app_icon_setter = super::app_icon::AppTitleIconSetter::new(
app_name.to_owned(),
native_options.icon_data.clone(),
);
Self {
frame,
last_auto_save: std::time::Instant::now(),
@ -387,7 +394,8 @@ impl EpiIntegration {
close: false,
can_drag_window: false,
window_state,
follow_system_theme,
follow_system_theme: native_options.follow_system_theme,
app_icon_setter,
}
}
@ -474,6 +482,8 @@ impl EpiIntegration {
) -> egui::FullOutput {
let frame_start = std::time::Instant::now();
self.app_icon_setter.update();
self.frame.info.window_info =
read_window_info(window, self.egui_ctx.pixels_per_point(), &self.window_state);
let raw_input = self.egui_winit.take_egui_input(window);

1
crates/eframe/src/native/mod.rs

@ -1,3 +1,4 @@
mod app_icon;
mod epi_integration;
pub mod run;

6
crates/eframe/src/native/run.rs

@ -702,7 +702,8 @@ mod glow_integration {
painter.max_texture_side(),
gl_window.window(),
system_theme,
self.native_options.follow_system_theme,
&self.app_name,
&self.native_options,
storage,
Some(gl.clone()),
#[cfg(feature = "wgpu")]
@ -1166,7 +1167,8 @@ mod wgpu_integration {
painter.max_texture_side().unwrap_or(2048),
&window,
system_theme,
self.native_options.follow_system_theme,
&self.app_name,
&self.native_options,
storage,
#[cfg(feature = "glow")]
None,

5
crates/egui_demo_app/src/main.rs

@ -22,6 +22,11 @@ fn main() -> Result<(), eframe::Error> {
initial_window_size: Some([1280.0, 1024.0].into()),
icon_data: Some(
eframe::IconData::try_from_png_bytes(&include_bytes!("../../../media/icon.png")[..])
.unwrap(),
),
#[cfg(feature = "wgpu")]
renderer: eframe::Renderer::Wgpu,

BIN
media/icon.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Loading…
Cancel
Save