diff --git a/apps/pdfcraft/Cargo.toml b/apps/pdfcraft/Cargo.toml index 94e1de896c..5e4010fcad 100644 --- a/apps/pdfcraft/Cargo.toml +++ b/apps/pdfcraft/Cargo.toml @@ -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. diff --git a/apps/pdfcraft/src/main.rs b/apps/pdfcraft/src/main.rs index d777ddc269..837a803e22 100644 --- a/apps/pdfcraft/src/main.rs +++ b/apps/pdfcraft/src/main.rs @@ -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 { + 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>, @@ -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); diff --git a/apps/pdfcraft/src/tests.rs b/apps/pdfcraft/src/tests.rs index ebd13dccc8..7c1f111b96 100644 --- a/apps/pdfcraft/src/tests.rs +++ b/apps/pdfcraft/src/tests.rs @@ -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;