diff --git a/src/inject/inject.cc b/src/inject/inject.cc index 3a072a4..65f959a 100644 --- a/src/inject/inject.cc +++ b/src/inject/inject.cc @@ -31,6 +31,8 @@ static unsigned char g_icon_png[] = { #include "Shlobj.h" +#include + namespace fs = std::filesystem; void init_inject_logger() { @@ -1123,6 +1125,63 @@ std::string getFontPath() { throw std::runtime_error("no font found"); } +// ── 原生标题栏修正 ────────────────────────────────────────────── +// GLFW 注册窗口类时按名字去 exe 资源里找 "GLFW_ICON" +// (见 breeze-glfw/src/win32_window.c 的 createNativeWindow),找不到就退回 +// IDI_APPLICATION —— 也就是 Windows 那个通用程序图标。而 src/inject/inject.rc +// 里图标组的名字实际是 "IDI_APP_ICON",两边对不上,所以标题栏图标一直是错的。 +// 另外 GLFW 不会开沉浸式深色标题栏,深色主题下最小化/最大化/关闭这三个按钮 +// 会按浅色标题栏渲染成黑字黑底,跟内容糊在一起。 +// +// Windows 10 20H1 之前的系统用属性 19,SDK 头文件里可能还没有定义 +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif +#define DWMWA_USE_IMMERSIVE_DARK_MODE_PRE_20H1 19 + +static bool is_light_mode() { + DWORD data = 0; + DWORD size = sizeof(data); + auto res = RegGetValueW( + HKEY_CURRENT_USER, + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize", + L"AppsUseLightTheme", RRF_RT_REG_DWORD, nullptr, &data, &size); + return res == ERROR_SUCCESS && data == 1; +} + +static void apply_native_window_style(HWND hwnd) { + if (!hwnd) { + return; + } + + const BOOL use_dark = is_light_mode() ? FALSE : TRUE; + if (FAILED(DwmSetWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE, + &use_dark, sizeof(use_dark)))) { + DwmSetWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE_PRE_20H1, + &use_dark, sizeof(use_dark)); + } + + // 按资源组的真实名字取图标,系统会从图标组里挑最接近请求尺寸的那一帧。 + // 做成 static 共享,也刻意不 DestroyIcon —— 窗口整个生命周期都在引用它, + // 等进程退出由系统回收。 + static HICON small_icon = static_cast( + LoadImageW(GetModuleHandleW(nullptr), L"IDI_APP_ICON", IMAGE_ICON, + GetSystemMetrics(SM_CXSMICON), + GetSystemMetrics(SM_CYSMICON), LR_DEFAULTCOLOR)); + static HICON big_icon = static_cast( + LoadImageW(GetModuleHandleW(nullptr), L"IDI_APP_ICON", IMAGE_ICON, + GetSystemMetrics(SM_CXICON), GetSystemMetrics(SM_CYICON), + LR_DEFAULTCOLOR)); + if (small_icon) { + SendMessageW(hwnd, WM_SETICON, ICON_SMALL, + reinterpret_cast(small_icon)); + } + if (big_icon) { + SendMessageW(hwnd, WM_SETICON, ICON_BIG, + reinterpret_cast(big_icon)); + } +} + void StartInjectUI() { if (auto r = ui::render_target::init_global(); !r) { spdlog::error("Failed to initialize global render target."); @@ -1138,6 +1197,7 @@ void StartInjectUI() { spdlog::error("Failed to initialize render target."); return; } + apply_native_window_style(static_cast(rt.hwnd())); nvgCreateFont(rt.nvg, "main", getFontPath().c_str()); rt.root->emplace_child(); rt.start_loop(); @@ -1183,6 +1243,7 @@ void ShowCrashDialog() { return; } + apply_native_window_style(static_cast(rt.hwnd())); nvgCreateFont(rt.nvg, "main", getFontPath().c_str()); auto error_ui = rt.root->emplace_child(); diff --git a/src/shell/script/binding_types_breeze_ui.cc b/src/shell/script/binding_types_breeze_ui.cc index bf26aa3..aa0112f 100644 --- a/src/shell/script/binding_types_breeze_ui.cc +++ b/src/shell/script/binding_types_breeze_ui.cc @@ -10,10 +10,22 @@ #include #include #include +#include #include "../utils.h" #include "spdlog/spdlog.h" +// Windows 10 20H1 之前的系统用属性 19,SDK 头文件里可能还没有定义 +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif +#define DWMWA_USE_IMMERSIVE_DARK_MODE_PRE_20H1 19 + +// resources/injector.ico 的原始内容,由 xmake 的 utils.bin2c 规则生成 +static unsigned char g_injector_ico[] = { +#include "injector.ico.h" +}; + namespace mb_shell::js { namespace { @@ -27,6 +39,87 @@ void set_owner_rt_recursive(const std::shared_ptr &widget, set_owner_rt_recursive(child, owner_rt); } } + +// 从 ico 数据里挑一帧最接近 want 尺寸的,直接交给系统造 HICON。 +// ico 里每一帧本身就是一份 RT_ICON 资源(BITMAPINFOHEADER + 像素 + AND 掩码), +// 所以 CreateIconFromResourceEx 可以直接吃,不需要自己解码。 +HICON load_icon_from_ico(const unsigned char *data, size_t size, int want) { + if (!data || size < 6) { + return nullptr; + } + auto rd16 = [](const unsigned char *p) { + return static_cast(p[0] | (p[1] << 8)); + }; + auto rd32 = [](const unsigned char *p) { + return static_cast(p[0] | (p[1] << 8) | (p[2] << 16) | + (static_cast(p[3]) << 24)); + }; + // ICONDIR: reserved=0, type=1(ICON), count + if (rd16(data) != 0 || rd16(data + 2) != 1) { + return nullptr; + } + const unsigned count = rd16(data + 4); + const unsigned char *frame = nullptr; + unsigned frame_size = 0, best_delta = ~0u; + for (unsigned i = 0; i < count; ++i) { + const unsigned char *entry = data + 6 + i * 16; + if (static_cast(entry - data) + 16 > size) { + break; + } + const unsigned w = entry[0] ? entry[0] : 256; + const unsigned h = entry[1] ? entry[1] : 256; + const unsigned bytes = rd32(entry + 8); + const unsigned offset = rd32(entry + 12); + if (w != h || static_cast(offset) + bytes > size) { + continue; + } + const unsigned delta = w > static_cast(want) + ? w - static_cast(want) + : static_cast(want) - w; + if (delta < best_delta) { + best_delta = delta; + frame = data + offset; + frame_size = bytes; + } + } + if (!frame) { + return nullptr; + } + return static_cast(CreateIconFromResourceEx( + const_cast(frame), frame_size, TRUE, 0x00030000, 0, 0, + LR_DEFAULTCOLOR)); +} + +// 窗口建好后补上系统标题栏相关的设置。GLFW 注册窗口类时找不到 GLFW_ICON 资源就会退回 +// IDI_APPLICATION(也就是 Windows 程序的那个通用图标),另外它也不会开启沉浸式深色标题栏, +// 于是深色主题下标题栏文字和最小化/最大化/关闭三个按钮会画成黑字黑底,跟背景糊在一起。 +void apply_native_window_style(HWND hwnd) { + if (!hwnd) { + return; + } + + const BOOL use_dark = is_light_mode() ? FALSE : TRUE; + if (FAILED(DwmSetWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE, &use_dark, + sizeof(use_dark)))) { + DwmSetWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE_PRE_20H1, &use_dark, + sizeof(use_dark)); + } + + // 图标做成进程内共享,省得每开一个窗口都多留两个句柄。 + // 也刻意不 DestroyIcon:窗口整个生命周期都会引用它,等进程退出由系统回收。 + static HICON small_icon = + load_icon_from_ico(g_injector_ico, sizeof(g_injector_ico), 16); + static HICON big_icon = + load_icon_from_ico(g_injector_ico, sizeof(g_injector_ico), 32); + if (small_icon) { + SendMessageW(hwnd, WM_SETICON, ICON_SMALL, + reinterpret_cast(small_icon)); + } + if (big_icon) { + SendMessageW(hwnd, WM_SETICON, ICON_BIG, + reinterpret_cast(big_icon)); + } +} } // namespace // Macro for getter/setter pairs with animation support @@ -741,6 +834,8 @@ breeze_ui::window::create_ex(std::string title, int width, int height, std::thread([win, on_close = std::move(on_close)]() { set_thread_name("breeze::js_window_renderer"); if (auto res = win->$render_target->init(); res) { + apply_native_window_style( + static_cast(win->$render_target->hwnd())); config::current->apply_fonts_to_nvg(win->$render_target->nvg); win->$render_target->show(); win->$render_target->start_loop(); diff --git a/xmake.lua b/xmake.lua index 6bb4a80..9c7d8ec 100644 --- a/xmake.lua +++ b/xmake.lua @@ -57,10 +57,14 @@ target("shell") add_defines("NOMINMAX", "WIN32_LEAN_AND_MEAN") add_packages("blook", "reflect-cpp", "wintoast", "yalantinglibs", "breeze-ui", "sentry-native", "watcher", "spdlog", "fmt", "breeze-js-runtime") - add_syslinks("oleacc", "ole32", "oleaut32", "uuid", "comctl32", "comdlg32", "gdi32", "user32", "shell32", "kernel32", "advapi32", "psapi", "Winhttp", "dbghelp") + add_syslinks("oleacc", "ole32", "oleaut32", "uuid", "comctl32", "comdlg32", "gdi32", "user32", "shell32", "kernel32", "advapi32", "psapi", "Winhttp", "dbghelp", "dwmapi") add_rules("utils.bin2obj", { extensions = {".js"} }) + add_rules("utils.bin2c", { + extensions = {".ico"} + }) + add_files("resources/injector.ico") set_version(version) set_configdir("src/shell") add_configfiles("src/shell/build_info.h.in") @@ -96,7 +100,7 @@ target("asan_test") target("inject") set_kind("binary") - add_syslinks("psapi", "user32", "shell32", "kernel32", "advapi32", "taskschd", "ole32", "oleaut32", "taskschd", "comsupp") + add_syslinks("psapi", "user32", "shell32", "kernel32", "advapi32", "taskschd", "ole32", "oleaut32", "taskschd", "comsupp", "dwmapi") add_files("src/inject/*.cc", "src/inject/*.rc") add_packages("breeze-ui", "spdlog", "fmt") set_basename("breeze")