From 21800ca5ae0d9e0a9626ca9cbafd30a8d72f40d5 Mon Sep 17 00:00:00 2001 From: Dominic Letz Date: Tue, 1 Sep 2026 15:35:09 +0200 Subject: [PATCH] Windows: load PNG icons for window/taskbar. Desktop.Window passes diode.png; LoadImage(IMAGE_ICON) only accepts .ico, so icon.create returned a null HICON and WM_SETICON was a no-op (blank title bar and taskbar). Decode PNG/JPEG via GDI+ and fall back to the host exe icon for the window class and default tray. Co-authored-by: Cursor --- docs/status/windows.md | 2 +- native/windows/CMakeLists.txt | 1 + native/windows/src/host_controller.cpp | 12 ++++-- native/windows/src/web_window.cpp | 4 ++ native/windows/src/win_util.hpp | 55 ++++++++++++++++++++++++++ 5 files changed, 69 insertions(+), 5 deletions(-) diff --git a/docs/status/windows.md b/docs/status/windows.md index 1ecfb61..7556a18 100644 --- a/docs/status/windows.md +++ b/docs/status/windows.md @@ -26,7 +26,7 @@ Release asset: `DesktopWebView-windows-x64.exe` (GitHub Releases; not Hex `priv/ | Tray / status item | done | `Shell_NotifyIcon`; E2E tray create | | Apple menu | n/a | Successful no-op | | Notifications | done | Balloon via tray when present; E2E | -| Icons from path / PNG | partial | Path/ICO via `LoadImage`; PNG→HICON deferred | +| Icons from path / PNG | done | ICO via `LoadImage`; PNG/JPEG via GDI+ `Bitmap::GetHICON`; class/default from exe | | OS events (reopen, open url/file) | partial | `system.open_url` done; OS reopen/file events not wired | | Locale / os_description | done | E2E | | Permission policy hybrid | done | E2E simulate + policy | diff --git a/native/windows/CMakeLists.txt b/native/windows/CMakeLists.txt index 04b56b3..b066f1e 100644 --- a/native/windows/CMakeLists.txt +++ b/native/windows/CMakeLists.txt @@ -60,6 +60,7 @@ target_link_libraries(DesktopWebView PRIVATE shell32 user32 gdi32 + gdiplus comctl32 advapi32 shlwapi diff --git a/native/windows/src/host_controller.cpp b/native/windows/src/host_controller.cpp index b8b7075..978a3da 100644 --- a/native/windows/src/host_controller.cpp +++ b/native/windows/src/host_controller.cpp @@ -661,7 +661,8 @@ jsonutil::Json HostController::menu_update(const jsonutil::Json& params) { } void HostController::update_tray_icon(TrayEntry& tray) { - HICON icon = LoadIconW(nullptr, IDI_APPLICATION); + HICON icon = extract_module_icon(); + if (!icon) icon = LoadIconW(nullptr, IDI_APPLICATION); if (!tray.icon_id.empty()) { auto it = icons_.find(tray.icon_id); if (it != icons_.end() && it->second.icon) icon = it->second.icon; @@ -694,10 +695,13 @@ jsonutil::Json HostController::icon_create(const jsonutil::Json& params) { auto id = next_id("icon"); IconEntry icon; if (auto path = jsonutil::get_string(params, "path")) { - icon.icon = static_cast(LoadImageW(nullptr, utf8_to_wide(*path).c_str(), IMAGE_ICON, 0, 0, - LR_LOADFROMFILE | LR_DEFAULTSIZE)); + // Prefer ICO via LoadImage; fall back to GDI+ for PNG/JPEG (diode.png etc.). + icon.icon = load_hicon_from_file(utf8_to_wide(*path)); + } else { + // Default / missing path: use the host executable's embedded icon. + icon.icon = extract_module_icon(); } - // png_base64 accepted for protocol compatibility; decoding deferred (status: partial). + // png_base64 still deferred; callers should pass a filesystem path for now. icons_[id] = std::move(icon); return jsonutil::Json{{"icon_id", id}}; } diff --git a/native/windows/src/web_window.cpp b/native/windows/src/web_window.cpp index cb95cfa..f22e2ef 100644 --- a/native/windows/src/web_window.cpp +++ b/native/windows/src/web_window.cpp @@ -22,6 +22,10 @@ void WebWindow::register_class() { wc.hCursor = LoadCursorW(nullptr, IDC_ARROW); wc.hbrBackground = reinterpret_cast(COLOR_WINDOW + 1); wc.lpszClassName = kClassName; + // Title-bar/taskbar icons come from the class (or WM_SETICON). Without these, + // Windows shows a blank default even when the .exe has an embedded icon. + wc.hIcon = extract_module_icon(); + wc.hIconSm = extract_module_icon(); RegisterClassExW(&wc); g_class_registered = true; } diff --git a/native/windows/src/win_util.hpp b/native/windows/src/win_util.hpp index f56bc8c..8cfed75 100644 --- a/native/windows/src/win_util.hpp +++ b/native/windows/src/win_util.hpp @@ -2,7 +2,11 @@ #include "win_prefix.hpp" +#include + #include +#include +#include #include #include @@ -45,3 +49,54 @@ inline bool is_absolute_path(const std::string& p) { if (p.size() >= 2 && std::isalpha(static_cast(p[0])) && p[1] == ':') return true; return p.size() >= 2 && p[0] == '\\' && p[1] == '\\'; } + +inline bool ends_with_ignore_case(const std::wstring& s, const wchar_t* suffix) { + const size_t n = wcslen(suffix); + if (s.size() < n) return false; + for (size_t i = 0; i < n; i++) { + wchar_t a = towlower(s[s.size() - n + i]); + wchar_t b = towlower(suffix[i]); + if (a != b) return false; + } + return true; +} + +// LoadImage(IMAGE_ICON) only accepts .ico. App icons are often PNG (e.g. diode.png); +// use GDI+ so window/taskbar icons are not left blank. +inline HICON load_hicon_from_file(const std::wstring& path) { + if (path.empty()) return nullptr; + + HICON icon = static_cast( + LoadImageW(nullptr, path.c_str(), IMAGE_ICON, 0, 0, LR_LOADFROMFILE | LR_DEFAULTSIZE)); + if (icon) return icon; + + if (!ends_with_ignore_case(path, L".png") && !ends_with_ignore_case(path, L".jpg") && + !ends_with_ignore_case(path, L".jpeg") && !ends_with_ignore_case(path, L".bmp") && + !ends_with_ignore_case(path, L".gif")) { + return nullptr; + } + + static ULONG_PTR gdiplus_token = 0; + static bool gdiplus_ready = false; + if (!gdiplus_ready) { + Gdiplus::GdiplusStartupInput input; + gdiplus_ready = Gdiplus::GdiplusStartup(&gdiplus_token, &input, nullptr) == Gdiplus::Ok; + } + if (!gdiplus_ready) return nullptr; + + Gdiplus::Bitmap bitmap(path.c_str()); + if (bitmap.GetLastStatus() != Gdiplus::Ok) return nullptr; + HICON from_png = nullptr; + if (bitmap.GetHICON(&from_png) != Gdiplus::Ok) return nullptr; + return from_png; +} + +inline HICON extract_module_icon() { + wchar_t module[MAX_PATH]{}; + HINSTANCE inst = GetModuleHandleW(nullptr); + if (!GetModuleFileNameW(inst, module, MAX_PATH)) return nullptr; + HICON icon = ExtractIconW(inst, module, 0); + // ExtractIcon returns 1 when the file has no icons. + if (!icon || icon == reinterpret_cast(static_cast(1))) return nullptr; + return icon; +}