Skip to content
Closed
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
61 changes: 61 additions & 0 deletions src/inject/inject.cc
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ static unsigned char g_icon_png[] = {

#include "Shlobj.h"

#include <dwmapi.h>

namespace fs = std::filesystem;

void init_inject_logger() {
Expand Down Expand Up @@ -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<HICON>(
LoadImageW(GetModuleHandleW(nullptr), L"IDI_APP_ICON", IMAGE_ICON,
GetSystemMetrics(SM_CXSMICON),
GetSystemMetrics(SM_CYSMICON), LR_DEFAULTCOLOR));
static HICON big_icon = static_cast<HICON>(
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<LPARAM>(small_icon));
}
if (big_icon) {
SendMessageW(hwnd, WM_SETICON, ICON_BIG,
reinterpret_cast<LPARAM>(big_icon));
}
}

void StartInjectUI() {
if (auto r = ui::render_target::init_global(); !r) {
spdlog::error("Failed to initialize global render target.");
Expand All @@ -1138,6 +1197,7 @@ void StartInjectUI() {
spdlog::error("Failed to initialize render target.");
return;
}
apply_native_window_style(static_cast<HWND>(rt.hwnd()));
nvgCreateFont(rt.nvg, "main", getFontPath().c_str());
rt.root->emplace_child<injector_ui_main>();
rt.start_loop();
Expand Down Expand Up @@ -1183,6 +1243,7 @@ void ShowCrashDialog() {
return;
}

apply_native_window_style(static_cast<HWND>(rt.hwnd()));
nvgCreateFont(rt.nvg, "main", getFontPath().c_str());

auto error_ui = rt.root->emplace_child<ui::flex_widget>();
Expand Down
95 changes: 95 additions & 0 deletions src/shell/script/binding_types_breeze_ui.cc
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,22 @@
#include <print>
#include <thread>
#include <Windows.h>
#include <dwmapi.h>

#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 {
Expand All @@ -27,6 +39,87 @@ void set_owner_rt_recursive(const std::shared_ptr<ui::widget> &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<unsigned>(p[0] | (p[1] << 8));
};
auto rd32 = [](const unsigned char *p) {
return static_cast<unsigned>(p[0] | (p[1] << 8) | (p[2] << 16) |
(static_cast<unsigned>(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<size_t>(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<size_t>(offset) + bytes > size) {
continue;
}
const unsigned delta = w > static_cast<unsigned>(want)
? w - static_cast<unsigned>(want)
: static_cast<unsigned>(want) - w;
if (delta < best_delta) {
best_delta = delta;
frame = data + offset;
frame_size = bytes;
}
}
if (!frame) {
return nullptr;
}
return static_cast<HICON>(CreateIconFromResourceEx(
const_cast<unsigned char *>(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<LPARAM>(small_icon));
}
if (big_icon) {
SendMessageW(hwnd, WM_SETICON, ICON_BIG,
reinterpret_cast<LPARAM>(big_icon));
}
}
} // namespace

// Macro for getter/setter pairs with animation support
Expand Down Expand Up @@ -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<HWND>(win->$render_target->hwnd()));
config::current->apply_fonts_to_nvg(win->$render_target->nvg);
win->$render_target->show();
win->$render_target->start_loop();
Expand Down
8 changes: 6 additions & 2 deletions xmake.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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")
Expand Down