Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/status/windows.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
1 change: 1 addition & 0 deletions native/windows/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ target_link_libraries(DesktopWebView PRIVATE
shell32
user32
gdi32
gdiplus
comctl32
advapi32
shlwapi
Expand Down
12 changes: 8 additions & 4 deletions native/windows/src/host_controller.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<HICON>(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}};
}
Expand Down
4 changes: 4 additions & 0 deletions native/windows/src/web_window.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ void WebWindow::register_class() {
wc.hCursor = LoadCursorW(nullptr, IDC_ARROW);
wc.hbrBackground = reinterpret_cast<HBRUSH>(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;
}
Expand Down
55 changes: 55 additions & 0 deletions native/windows/src/win_util.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@

#include "win_prefix.hpp"

#include <gdiplus.h>

#include <cctype>
#include <cwchar>
#include <cwctype>
#include <optional>
#include <string>

Expand Down Expand Up @@ -45,3 +49,54 @@ inline bool is_absolute_path(const std::string& p) {
if (p.size() >= 2 && std::isalpha(static_cast<unsigned char>(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<HICON>(
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<HICON>(static_cast<uintptr_t>(1))) return nullptr;
return icon;
}
Loading