fix(app): set Windows taskbar and Alt+Tab big icon (ICON_BIG) (#3818) (#4681)

This commit is contained in:
jonathanwxh-cell authored and GitHub committed 2026-11-09 18:26:07 +08:00
1 parent e24780c7bd
commit d4828fe541
3 files changed
+82

No files matched your search

+4
View File
@@ -64,6 +64,10 @@ windows = { version = "0.62", default-features = false, features = ["std", "UI_V
# socket. `std` has no Unix sockets on Windows, and calling Winsock directly needs unsafe, which
# our workspace forbids. Already in the lockfile through zbus; pinned exactly, review before bumping.
uds_windows = "=1.2.1"
# Full-resolution taskbar and Alt+Tab icon (`ICON_BIG`, issue #3818). Set both window icons
# directly so eframe cannot replace ICON_BIG with a system-sized copy. Already in the tree
# through eframe and covered by the workspace's winit patch.
winit = { version = "0.30", default-features = false }
[build-dependencies]
# Icon + VERSIONINFO for pdfcraft.exe; build.rs only uses it when targeting Windows.
+53
View File
@@ -340,7 +340,15 @@ fn native_options(integrated: bool, app_frame: bool, renderer: eframe::Renderer)
.with_drag_and_drop(true)
// Wayland app id: matches packaging/linux/ai.storyteller.pdfcraft.desktop.
.with_app_id(APP_ID);
// eframe replaces ICON_BIG with a system-sized icon on its first active frame.
// An explicitly empty icon disables that setter (None selects eframe's default).
// Windows installs both sizes directly through winit in app_creator instead.
#[cfg(target_os = "windows")]
{
viewport = viewport.with_icon(egui::IconData::default());
}
// Dock, taskbar, Alt-Tab and launcher icon when running unbundled.
#[cfg(not(target_os = "windows"))]
match eframe::icon_data::from_png_bytes(APP_ICON_PNG) {
Ok(icon) => viewport = viewport.with_icon(icon),
Err(e) => log::warn!("app icon: {e}"),
@@ -391,6 +399,48 @@ fn native_wayland(_: &eframe::CreationContext<'_>) -> bool {
false
}
/// Decode `png` as a [`winit::window::Icon`] for the Windows taskbar and Alt+Tab (`ICON_BIG`).
#[cfg(target_os = "windows")]
fn taskbar_icon(png: &[u8]) -> Option<winit::window::Icon> {
let icon = match eframe::icon_data::from_png_bytes(png) {
Ok(icon) => icon,
Err(e) => {
log::warn!("taskbar icon: {e}");
return None;
}
};
match winit::window::Icon::from_rgba(icon.rgba, icon.width, icon.height) {
Ok(icon) => Some(icon),
Err(e) => {
log::warn!("taskbar icon: {e}");
None
}
}
}
/// Install the decoded [`winit::window::Icon`] from `png` via `set` when decoding succeeds.
#[cfg(target_os = "windows")]
fn install_taskbar_icon(png: &[u8], set: impl FnOnce(winit::window::Icon)) {
if let Some(icon) = taskbar_icon(png) {
set(icon);
}
}
/// Install both window icons before the first frame. eframe's icon setter is disabled in
/// `native_options`, so it cannot replace the full-resolution taskbar/Alt+Tab icon (#3818).
#[cfg(target_os = "windows")]
fn set_taskbar_icon(window: &winit::window::Window) {
use winit::platform::windows::{IconExtWindows as _, WindowExtWindows as _};
install_taskbar_icon(APP_ICON_PNG, |icon| window.set_taskbar_icon(Some(icon)));
// The title bar's icon: the small image drawn for this scale in the exe's own icon (build.rs).
let side = (16.0 * window.scale_factor()).round().clamp(16.0, 256.0) as u32;
match winit::window::Icon::from_resource(1, Some(winit::dpi::PhysicalSize::new(side, side))) {
Ok(icon) => window.set_window_icon(Some(icon)),
Err(e) => log::warn!("window icon: {e}"),
}
}
/// Build the [`eframe::AppCreator`] closure that initializes [`PdfCraftApp`] once the window and renderer are ready.
fn app_creator<'a>(
launch: Launch,
started: Rc<Cell<bool>>,
@@ -418,6 +468,9 @@ fn app_creator<'a>(
// it is set to On or Off.
#[cfg(target_os = "windows")]
{
if let Some(window) = cc.winit_window() {
set_taskbar_icon(window);
}
app.motion.source = Some(motion::source());
}
app.set_startup_update_check(true);
+25
View File
@@ -137,6 +137,31 @@ fn the_desktop_frame_is_turned_off_only_when_the_app_draws_one() {
assert_eq!(native_options(false, true, eframe::Renderer::Glow).viewport.decorations, Some(false));
}
#[test]
fn the_window_and_windows_taskbar_icons_load_from_the_bundled_png() {
let icon = eframe::icon_data::from_png_bytes(super::APP_ICON_PNG).expect("the bundled PNG decodes as the window icon");
assert!(icon.width > 0 && icon.height > 0);
assert_eq!(icon.rgba.len(), (icon.width as usize) * (icon.height as usize) * 4);
#[cfg(not(target_os = "windows"))]
assert_eq!(super::native_options(false, false, eframe::Renderer::Glow).viewport.icon.as_deref(), Some(&icon));
#[cfg(target_os = "windows")]
{
// None would load eframe's default icon; only the explicit empty sentinel prevents
// its first-frame setter from overwriting our full-resolution ICON_BIG.
for renderer in [eframe::Renderer::Glow, eframe::Renderer::Wgpu] {
let viewport = super::native_options(false, false, renderer).viewport;
assert_eq!(viewport.icon.as_deref(), Some(&egui::IconData::default()));
}
assert_eq!((icon.width, icon.height), (256, 256));
let mut installed = None;
super::install_taskbar_icon(super::APP_ICON_PNG, |icon| installed = Some(icon));
assert!(installed.is_some(), "issue #3818: ICON_BIG loads and installs from APP_ICON_PNG");
let mut bad = None;
super::install_taskbar_icon(b"not a png", |icon| bad = Some(icon));
assert!(bad.is_none());
}
}
#[test]
fn a_software_renderer_is_shown_to_the_user() {
use eframe::wgpu::DeviceType;