├── .clangd ├── .gitattributes ├── .github └── workflows │ └── xmake.yml ├── .gitignore ├── .gitmodules ├── .vscode └── settings.json ├── CONFIG.md ├── DONATE.md ├── LICENSE ├── README.md ├── README_zh.md ├── dependencies ├── blook.lua ├── glfw.lua ├── quickjs-ng.lua └── reflect-cpp.lua ├── resources ├── animated-preview1.webp ├── bloom.webp ├── breeze.html ├── code.webp ├── icon-small.png ├── icon.7z ├── icon.png ├── icon.webp ├── inject-en.webp ├── inject.webp ├── preview1.webp └── schema.json ├── scripts ├── bindgen │ ├── .gitignore │ ├── c-type-parser.ts │ ├── clang-ast.d.ts │ ├── generate-bindings.bat │ ├── index.ts │ ├── package.json │ ├── quickjs-types.txt │ ├── typegen.ts │ └── yarn.lock ├── debug.cmd ├── rebuild.ps1 └── right_click.ahk ├── src ├── inject │ ├── data_directory.inc │ └── inject.cc ├── shell │ ├── build_info.h.in │ ├── config.cc │ ├── config.h │ ├── contextmenu │ │ ├── contextmenu.cc │ │ ├── contextmenu.h │ │ ├── menu_render.cc │ │ ├── menu_render.h │ │ ├── menu_widget.cc │ │ └── menu_widget.h │ ├── entry.cc │ ├── entry.h │ ├── error_handler.cc │ ├── error_handler.h │ ├── fix_win11_menu.cc │ ├── fix_win11_menu.h │ ├── logger.cc │ ├── logger.h │ ├── res_string_loader.cc │ ├── res_string_loader.h │ ├── script │ │ ├── FileWatch.hpp │ │ ├── binding_qjs.h │ │ ├── binding_types.cc │ │ ├── binding_types.d.ts │ │ ├── binding_types.h │ │ ├── binding_types_com.cc │ │ ├── quickjspp.cc │ │ ├── quickjspp.hpp │ │ ├── script.cc │ │ ├── script.h │ │ └── script.js │ ├── utils.cc │ ├── utils.h │ ├── window_proc_hook.cc │ └── window_proc_hook.h ├── ui │ ├── animator.cc │ ├── animator.h │ ├── extra_widgets.cc │ ├── extra_widgets.h │ ├── hbitmap_utils.cc │ ├── hbitmap_utils.h │ ├── nanosvg.cc │ ├── nanovg_wrapper.h │ ├── swcadef.h │ ├── ui.cc │ ├── ui.h │ ├── widget.cc │ └── widget.h └── ui_test │ └── test.cc └── xmake.lua /.clangd: -------------------------------------------------------------------------------- 1 | CompileFlags: 2 | Add: ["-std:latest", "/clang:-std=c++23"] -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.h linguist-vendored 2 | *.c linguist-vendored -------------------------------------------------------------------------------- /.github/workflows/xmake.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | permissions: write-all 3 | 4 | on: 5 | push: 6 | branches: [ "master" ] 7 | pull_request: 8 | branches: [ "master" ] 9 | release: 10 | types: [created] 11 | 12 | jobs: 13 | build: 14 | runs-on: windows-2025 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | with: 19 | submodules: recursive 20 | 21 | - uses: xmake-io/github-action-setup-xmake@v1 22 | with: 23 | xmake-version: latest 24 | actions-cache-folder: '.xmake-cache' 25 | actions-cache-key: 'ci' 26 | package-cache: true 27 | package-cache-key: windows-2025 28 | # build-cache: true 29 | # build-cache-key: ${{ matrix.os }}-${{ matrix.build_type }} 30 | 31 | - name: Xmake configure 32 | run: | 33 | xmake config --yes --toolchain=clang-cl --mode=releasedbg 34 | 35 | - name: build-releasedbg 36 | run: | 37 | xmake b --yes --verbose inject 38 | xmake b --yes --verbose shell 39 | 40 | - name: Upload Artifacts 41 | uses: actions/upload-artifact@v4.6.0 42 | with: 43 | path: ./build/windows/x64/ 44 | name: windows-build 45 | 46 | - name: Create Archive 47 | if: github.event_name == 'release' 48 | run: | 49 | Compress-Archive -Path ./build/windows/* -DestinationPath windows-build-pdb.zip 50 | Remove-Item -Path ./build/windows/x64/releasedbg/*.pdb -Force 51 | Remove-Item -Path ./build/windows/x64/releasedbg/ui.lib -Force 52 | Compress-Archive -Path ./build/windows/* -DestinationPath windows-build.zip 53 | 54 | - name: Upload Release Assets 55 | if: github.event_name == 'release' 56 | uses: softprops/action-gh-release@v1 57 | with: 58 | files: | 59 | windows-build.zip 60 | windows-build-pdb.zip 61 | token: ${{ secrets.GITHUB_TOKEN }} 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | compile_commands.json 3 | .xmake 4 | .cache 5 | build_info.h 6 | /test -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "blook"] 2 | path = dependencies/blook 3 | url = https://github.com/std-microblock/blook 4 | [submodule "dependencies/glfw"] 5 | path = dependencies/glfw 6 | url = https://github.com/breeze-shell/glfw 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cmake.ignoreCMakeListsMissing": true, 3 | "files.associations": { 4 | "xstring": "cpp" 5 | } 6 | } -------------------------------------------------------------------------------- /CONFIG.md: -------------------------------------------------------------------------------- 1 | # 配置文件格式说明 2 | 3 | 本项目的配置文件采用 JSON 格式,推荐使用 VSCode 进行编辑。 4 | 5 | 本项目配置文件默认位于 `%USERPROFILE%/.breeze-shell/config.json`。 6 | 7 | 编辑配置文件并保存后,插件将会自动重载配置,无需重新启动。 8 | 9 | **如果保存后弹出了黑窗口,这大概是因为你的配置文件有错误,请阅读黑窗口内的报错信息并修复错误** 10 | 11 | ## Schema 12 | 13 | Breeze Shell 配置文件的 JSON Schema 位于 14 | [resources/schema.json](./resources/schema.json),在配置文件内写入 15 | 16 | ```json 17 | { 18 | "$schema": "https://raw.githubusercontent.com/std-microblock/breeze-shell/refs/heads/master/resources/schema.json" 19 | } 20 | ``` 21 | 22 | 即可在 VSCode 中看到配置文件类型检查及补全。 23 | 24 | ## 配置文件结构 25 | 26 | 以下为一份带有注释的完整默认 JSON 配置,注意其**不能**直接填入 config.json 27 | 当中,因为配置文件解析当前不支持注释 28 | 29 | ```json5 30 | { 31 | "context_menu": { 32 | "theme": { 33 | // 在 Windows 11 下使用 DWM 圆角而不是 SetWindowRgn 圆角 34 | "use_dwm_if_available": true, 35 | // 启用亚克力背景效果 36 | "acrylic": true, 37 | // 圆角大小,仅在不使用 DWM 圆角时生效 38 | "radius": 6.0, 39 | // 字体大小,可调整此项以对齐缩放后的整数倍率字体大小以避免模糊 40 | "font_size": 14.0, 41 | // 项高度 42 | "item_height": 23.0, 43 | // 项间距 44 | "item_gap": 3.0, 45 | // 项圆角大小 46 | "item_radius": 5.0, 47 | // 外边距 48 | "margin": 5.0, 49 | // 内边距 50 | "padding": 6.0, 51 | // 文笔内边距 52 | "text_padding": 8.0, 53 | // 图标内边距 54 | "icon_padding": 4.0, 55 | // 右侧图标(展开图标)边距 56 | "right_icon_padding": 20.0, 57 | // 横排按钮间距(此处为负值以抵消项边距的效果) 58 | "multibutton_line_gap": -6.0, 59 | // 在亮色主题下的亚克力背景颜色 60 | "acrylic_color_light": "#fefefe00", 61 | // 在暗色主题下的亚克力背景颜色 62 | "acrylic_color_dark": "#28282800", 63 | // 背景透明度 64 | "background_opacity":1.0, 65 | // 动画相关 66 | "animation": { 67 | // 菜单项动画 68 | "item": { 69 | // animated_float_conf: 通用动画配置 70 | "opacity": { 71 | // 持续时长 72 | "duration": 200.0, 73 | // 动画曲线 74 | // 可为: 75 | // mutation (关闭动画) 76 | // linear (线性) 77 | // ease_in, ease_out, ease_in_out (三种缓动曲线) 78 | "easing": "ease_in_out", 79 | // 对延迟时间的缩放 80 | // 即:如果本来是在开始总动画 50ms 后显示该动画, 81 | // 若 delay_scale 为 2 则在 100ms 后才显示 82 | "delay_scale": 1.0 83 | }, 84 | // 同 opacity,以下均省略 85 | "x": animated_float_conf, 86 | "y": animated_float_conf, 87 | "width": animated_float_conf 88 | }, 89 | // 主菜单的背景 90 | "main_bg": { 91 | "opacity": animated_float_conf, 92 | "x": animated_float_conf, 93 | "y": animated_float_conf, 94 | "w": animated_float_conf, 95 | "h": animated_float_conf 96 | }, 97 | // 子菜单的背景,同主菜单 98 | "submenu_bg": { 99 | ... 100 | } 101 | } 102 | }, 103 | // 启用垂直同步 104 | "vsync": true, 105 | // 不替换 owner draw 的菜单 106 | "ignore_owner_draw": true, 107 | // 在向上展开时将所有项反向 108 | "reverse_if_open_to_up": true, 109 | // 调试选项,搜索更大范围的图标,不建议打开 110 | "search_large_dwItemData_range": false, 111 | // 定位相关 112 | "position": { 113 | // 竖直边距 114 | "padding_vertical": 20, 115 | // 水平边距 116 | "padding_horizontal": 0 117 | } 118 | }, 119 | 120 | // 开启调试窗口 121 | "debug_console": false, 122 | 123 | // 主字体 124 | "font_path_main": "C:\\WINDOWS\\Fonts\\segoeui.ttf", 125 | // 副字体 126 | "font_path_fallback": "C:\\WINDOWS\\Fonts\\msyh.ttc", 127 | // 使用 hook 方式加载更多 resid 128 | "res_string_loader_use_hook": false, 129 | // 调试选项,避免更改 UI 窗口大小 130 | "avoid_resize_ui": false, 131 | // 插件加载顺序,在越前面的越先加载 132 | // 格式为插件的无拓展名文件名 133 | // 如:Windows 11 Icon Pack 134 | "plugin_load_order": [], 135 | // 全局默认动画效果 136 | "default_animation": animated_float_conf 137 | } 138 | ``` 139 | 140 | ## 示例配置文件 141 | 142 | #### 禁用所有动画 143 | 144 | ```json 145 | { 146 | "default_animation": { 147 | "easing": "mutation" 148 | } 149 | } 150 | ``` 151 | -------------------------------------------------------------------------------- /DONATE.md: -------------------------------------------------------------------------------- 1 | ### Donate to this project 2 | Donate to me if you like the project~ 3 | 4 | #### 中国大陆 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!WARNING] 2 | > This project is still in active development. File a bug report if you meet 3 | > any!\ 4 | > 此项目仍在开发阶段,如果遇到问题请发送 Issue 5 | > 6 | > Both English and Chinese issues are accepted. 7 | > Issue 中使用中文或英文均可 8 | 9 | [中文](./README_zh.md) [Donate Me](./DONATE.md) [Discord](https://discord.gg/MgpHk8pa3d) 10 | 11 |
12 | 13 |

Breeze Shell

14 |
Bring fluency & delication back to Windows
15 |
16 | 17 |
18 |
19 | 20 | Breeze is an **alternative context menu** for Windows 10 and Windows 11. 21 | 22 | ## Fluent 23 | Breeze is designed with animations in mind. 24 | 25 | 26 | 27 | All animations are configurable and can be scaled and disabled as you want. 28 | ## Extensible 29 | 30 | Empowered by the embedded JavaScript script api, Breeze enables you to extend 31 | the functionalities of your context menu in a few lines of code. 32 | 33 | ```javascript 34 | shell.menu_controller.add_menu_listener((e) => { 35 | e.menu.add({ 36 | type: "button", 37 | name: "Shuffle Buttons", 38 | action: () => { 39 | for (const item of menus) { 40 | item.set_position(Math.floor(menus.length * Math.random())); 41 | } 42 | }, 43 | }, 0); 44 | }); 45 | ``` 46 | 47 | [See full bindings →](./src/shell/script/binding_types.d.ts) 48 | 49 | Send pull requests to [this repo](https://github.com/breeze-shell/plugins) to add your script to the plugin market! 50 | 51 | ## Configurable 52 | Breeze shell exposed a bunch of configurations ranging from item height to background radius method. Customize them as you need. 53 | 54 | [Configuration Document →](./CONFIG.md) 55 | 56 | The config menu of breeze-shell can be found as you open your `Data Folder` and right-click anywhere inside it. 57 | 58 | ## Lightweight & Fast 59 | 60 | Breeze uses breeze-ui, which is implemented to be a cross-platform, simple, 61 | animation-friendly and fast ui library for modern C++, with the support of both 62 | NanoVG and ThorVG render context. This allowed Breeze to have a delicated user 63 | interface in ~2MiB. 64 | 65 | # Try it out! 66 | 67 | Download and extract the zip, and Run `breeze.exe`. 68 | 69 | ![image](https://github.com/user-attachments/assets/44324c0c-ea4e-4f45-a846-9c8ae2eab001) 70 | 71 | 72 | # Building 73 | 74 | Breeze uses xmake. You'd have to install xmake in your computer first. Then, 75 | type `xmake` in the project dir and follow the instructions. Both clang-cl and 76 | MSVC 2019+ can build this project. 77 | 78 | # Developing 79 | 80 | After building successfully once, you can oprn the project dir in VSCode for 81 | development. Install clangd plugin for full intellisense. 82 | 83 | # Credits 84 | 85 | #### Third-party libraries 86 | - https://github.com/std-microblock/blook 87 | - https://github.com/quickjs-ng/quickjs 88 | - https://github.com/ftk/quickjspp 89 | - https://github.com/getml/reflect-cpp 90 | - https://github.com/glfw/glfw 91 | - https://github.com/Dav1dde/glad 92 | - https://github.com/memononen/nanovg 93 | - https://github.com/memononen/nanosvg 94 | - https://github.com/freetype/freetype 95 | 96 | #### Others 97 | - [@lipraty](https://github.com/lipraty) - Icon Design 98 | - [moudey/Shell](https://github.com/moudey/Shell) - Inspiration 99 | -------------------------------------------------------------------------------- /README_zh.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

Breeze Shell

4 |
为 Windows 重新带来流畅与精致体验
5 |
6 | 7 |
8 |
9 | 10 | Breeze 是专为 Windows 10/11 设计的现代化**次世代右键菜单解决方案**。 11 | 12 | ## 丝滑流畅 13 | 14 | Breeze 以交互动画为核心设计理念。 15 | 16 | 17 | 18 | ## 无限扩展 19 | 20 | 通过嵌入式 JavaScript 脚本 API,您可以用寥寥数行代码为右键菜单增添全新功能。 21 | 22 | ```javascript 23 | shell.menu_controller.add_menu_listener((e) => { 24 | e.menu.add({ 25 | type: "button", 26 | name: "Shuffle Buttons", 27 | action: () => { 28 | for (const item of menus) { 29 | item.set_position(Math.floor(menus.length * Math.random())); 30 | } 31 | }, 32 | }, 0); 33 | }); 34 | ``` 35 | 36 | [查看完整 API 文档 →](./src/shell/script/binding_types.d.ts) 37 | 38 | ## 轻量高速 39 | 40 | Breeze 基于自研 breeze-ui 框架实现,这是一个跨平台、简洁优雅、动画友好的现代 C++ UI 库,支持 NanoVG 和 ThorVG 双渲染后端。这使得 Breeze 能在约 2MB 的体积下实现精致的视觉体验。 41 | 42 | # 立即体验 43 | 44 | 从 Releases 下载,然后解压压缩包并运行 `breeze.exe` 45 | 46 | # 构建指南 47 | 48 | 本项目使用 xmake 构建系统。请先安装 xmake,在项目根目录执行 `xmake` 命令并按提示操作。支持 clang-cl 和 MSVC 2019+ 编译器。 49 | 50 | # 开发指南 51 | 52 | 首次构建成功后,可使用 VSCode 打开项目进行开发。建议安装 clangd 插件以获得完整的代码智能提示。 53 | -------------------------------------------------------------------------------- /dependencies/blook.lua: -------------------------------------------------------------------------------- 1 | package("blook") 2 | add_deps("cmake") 3 | add_syslinks("advapi32") 4 | set_sourcedir(path.join(os.scriptdir(), "blook")) 5 | on_install(function (package) 6 | local fcdir = package:cachedir() .. "/fetchcontent" 7 | import("package.tools.cmake").install(package, { 8 | "-DCMAKE_INSTALL_PREFIX=" .. package:installdir(), 9 | "-DCMAKE_PREFIX_PATH=" .. package:installdir(), 10 | "-DFETCHCONTENT_QUIET=OFF", 11 | "-DFETCHCONTENT_BASE_DIR=" .. fcdir, 12 | }) 13 | 14 | os.cp("include/blook/**", package:installdir("include/blook/")) 15 | os.cp("external/zasm/zasm/include/**", package:installdir("include/zasm/")) 16 | os.cp(fcdir .. "/zydis-src/dependencies/zycore/include/**", package:installdir("include/zycore/")) 17 | os.cp(package:buildir() .. "/blook.lib", package:installdir("lib")) 18 | os.cp(package:buildir() .. "/external/zasm/zasm.lib", package:installdir("lib")) 19 | end) 20 | package_end() -------------------------------------------------------------------------------- /dependencies/glfw.lua: -------------------------------------------------------------------------------- 1 | package("breeze-glfw") 2 | set_base("glfw") 3 | 4 | set_urls("https://github.com/breeze-shell/glfw.git") 5 | 6 | add_versions("2025.04.13", "71513247ae3cc7eca66ca88f0789c64b5e693115") 7 | -------------------------------------------------------------------------------- /dependencies/quickjs-ng.lua: -------------------------------------------------------------------------------- 1 | package("quickjs-ng") 2 | set_homepage("https://github.com/quickjs-ng/quickjs") 3 | set_description("QuickJS, the Next Generation: a mighty JavaScript engine") 4 | set_license("MIT") 5 | 6 | add_urls("https://github.com/quickjs-ng/quickjs/archive/refs/tags/$(version).tar.gz", 7 | "https://github.com/quickjs-ng/quickjs.git", {submodules = false}) 8 | 9 | add_versions("v0.8.0", "7e60e1e0dcd07d25664331308a2f4aee2a88d60d85896e828d25df7c3d40204e") 10 | 11 | add_configs("libc", {description = "Build standard library modules as part of the library", default = false, type = "boolean"}) 12 | 13 | if is_plat("linux", "bsd") then 14 | add_syslinks("m", "pthread") 15 | end 16 | 17 | add_deps("cmake") 18 | 19 | if on_check then 20 | on_check("windows", function (package) 21 | local vs_toolset = package:toolchain("msvc"):config("vs_toolset") 22 | if vs_toolset then 23 | local vs_toolset_ver = import("core.base.semver").new(vs_toolset) 24 | local minor = vs_toolset_ver:minor() 25 | assert(minor and minor >= 30, "package(quickjs-ng) require vs_toolset >= 14.3") 26 | end 27 | end) 28 | on_check("wasm", "cross", function (package) 29 | if package:version():eq("0.8.0") then 30 | raise("package(quickjs-ng v0.8.0) unsupported platform") 31 | end 32 | end) 33 | end 34 | 35 | on_install("!iphoneos and (!windows or windows|!x86)", function (package) 36 | io.replace("CMakeLists.txt", "xcheck_add_c_compiler_flag(-Werror)", "", {plain = true}) 37 | io.replace("CMakeLists.txt", "if(NOT WIN32 AND NOT EMSCRIPTEN)", "if(0)", {plain = true}) 38 | 39 | local configs = {} 40 | table.insert(configs, "-DCMAKE_BUILD_TYPE=" .. (package:is_debug() and "Debug" or "Release")) 41 | if package:is_plat("windows") then 42 | if false then 43 | -- add /debug to link flags 44 | table.insert(configs, "-DCMAKE_EXE_LINKER_FLAGS_RELEASE=\"/DEBUG\"") 45 | table.insert(configs, "-DCMAKE_SHARED_LINKER_FLAGS_RELEASE=\"/DEBUG\"") 46 | 47 | -- also add /DEBUG to cflags, and /Zi to cxxflags 48 | table.insert(configs, "-DCMAKE_C_FLAGS_RELEASE=/Zi /DEBUG") 49 | table.insert(configs, "-DCMAKE_CXX_FLAGS_RELEASE=/Zi /DEBUG") 50 | end 51 | end 52 | 53 | table.insert(configs, "-DBUILD_SHARED_LIBS=" .. (package:config("shared") and "ON" or "OFF")) 54 | table.insert(configs, "-DCONFIG_ASAN=" .. (package:config("asan") and "ON" or "OFF")) 55 | table.insert(configs, "-DCONFIG_MSAN=" .. (package:config("msan") and "ON" or "OFF")) 56 | table.insert(configs, "-DCONFIG_UBSAN=" .. (package:config("ubsan") and "ON" or "OFF")) 57 | table.insert(configs, "-DBUILD_QJS_LIBC=" .. (package:config("libc") and "ON" or "OFF")) 58 | if package:config("shared") and package:is_plat("windows") then 59 | table.insert(configs, "-DCMAKE_WINDOWS_EXPORT_ALL_SYMBOLS=ON") 60 | end 61 | import("package.tools.cmake").install(package, configs) 62 | 63 | if package:is_plat("windows") and package:is_debug() then 64 | local dir = package:installdir(package:config("shared") and "bin" or "lib") 65 | os.vcp(path.join(package:buildir(), "*.pdb"), dir) 66 | end 67 | end) 68 | 69 | on_test(function (package) 70 | assert(package:has_cfuncs("JS_NewRuntime", {includes = "quickjs.h"})) 71 | end) -------------------------------------------------------------------------------- /dependencies/reflect-cpp.lua: -------------------------------------------------------------------------------- 1 | package("reflect-cpp") 2 | set_homepage("https://github.com/getml/reflect-cpp") 3 | set_description("A C++20 library for fast serialization, deserialization and validation using reflection. Supports JSON, BSON, CBOR, flexbuffers, msgpack, TOML, XML, YAML / msgpack.org[C++20]") 4 | set_license("MIT") 5 | 6 | add_urls("https://github.com/getml/reflect-cpp/archive/refs/tags/$(version).tar.gz", 7 | "https://github.com/getml/reflect-cpp.git", {submodules = false}) 8 | 9 | add_versions("v0.17.0", "08b6406cbe4c6c14ff1a619fe93a94f92f6d9eb22213d93529ad975993945e45") 10 | add_versions("v0.16.0", "a84d94dbd353d788926d6e54507b44c046863f7bc4ecb35afe0338374a68a77d") 11 | add_versions("v0.14.1", "639aec9d33025703a58d32c231ab1ab474c0cc4fb0ff90eadcaffb49271c41cd") 12 | add_versions("v0.14.0", "ea92a2460a71184b7d4fa4e9baad9910efad092df78b114459a7d6b0ee558d3c") 13 | add_versions("v0.13.0", "a7a31832fe8bbaa7f7299da46dfd4ccc8b99a13242e16a1d93f8669de1fca9c6") 14 | add_versions("v0.12.0", "13d448dd5eaee13ecb7ab5cb61cb263c7111ba75230503adc823a888f68e1eaa") 15 | add_versions("v0.11.1", "e45f112fb3f14507a4aa53b99ae2d4ab6a4e7b2d5f04dd06fec00bf7faa7bbdc") 16 | add_versions("v0.10.0", "d2c8876d993ddc8c57c5804e767786bdb46a2bdf1a6cd81f4b14f57b1552dfd7") 17 | 18 | add_patches("0.16.0", "patches/0.16.0/cmake.patch", "1b2a6e0ed81dd0bd373bd1daaf52010de965f3829e5e19406c53e8ebf0a5b9fc") 19 | add_patches("0.11.1", "patches/0.11.1/cmake.patch", "a43ae2c6de455054ab860adfb309da7bd376c31c493c8bab0ebe07aae0805205") 20 | add_patches("0.10.0", "patches/0.10.0/cmake.patch", "b8929c0a13bd4045cbdeea0127e08a784e2dc8c43209ca9f056fff4a3ab5c4d3") 21 | 22 | add_configs("bson", {description = "Enable Bson Support.", default = false, type = "boolean", readonly = true}) 23 | add_configs("yyjson", {description = "Enable yyjson Support.", default = true, type = "boolean"}) 24 | add_configs("cbor", {description = "Enable Cbor Support.", default = false, type = "boolean"}) 25 | add_configs("flatbuffers", {description = "Enable Flexbuffers Support.", default = false, type = "boolean"}) 26 | add_configs("msgpack", {description = "Enable Msgpack Support.", default = false, type = "boolean"}) 27 | add_configs("xml", {description = "Enable Xml Support.", default = false, type = "boolean"}) 28 | add_configs("toml", {description = "Enable Toml Support.", default = false, type = "boolean"}) 29 | add_configs("yaml", {description = "Enable Yaml Support.", default = false, type = "boolean"}) 30 | add_configs("ubjson", {description = "Enable UBJSON Support.", default = false, type = "boolean"}) 31 | if is_plat("windows") then 32 | add_configs("shared", {description = "Build shared library.", default = false, type = "boolean", readonly = true}) 33 | end 34 | 35 | on_check(function (package) 36 | if package:is_plat("windows") then 37 | local vs = package:toolchain("msvc"):config("vs") 38 | assert(vs and tonumber(vs) >= 2022, "package(reflect-cpp): need vs >= 2022") 39 | else 40 | assert(package:check_cxxsnippets({test = [[ 41 | #include 42 | #include 43 | #include 44 | void test() { 45 | constexpr std::string_view message = "Hello, C++20!"; 46 | for (char c : std::views::filter(message, [](char c) { return std::islower(c); })) 47 | std::cout << std::source_location::current().line() << ": " << c << '\n'; 48 | } 49 | ]]}, {configs = {languages = "c++20"}}), "package(reflect-cpp) Require at least C++20.") 50 | end 51 | end) 52 | 53 | on_load(function (package) 54 | if package:config("yyjson") then 55 | package:add("deps", "yyjson") 56 | end 57 | 58 | if package:config("cbor") then 59 | package:add("deps", "tinycbor") 60 | end 61 | 62 | if package:config("flatbuffers") then 63 | package:add("deps", "flatbuffers") 64 | end 65 | 66 | if package:config("msgpack") then 67 | package:add("deps", "msgpack-c") 68 | end 69 | 70 | if package:config("xml") then 71 | package:add("deps", "pugixml") 72 | end 73 | 74 | if package:config("toml") then 75 | package:add("deps", "toml++") 76 | end 77 | 78 | if package:config("yaml") then 79 | package:add("deps", "yaml-cpp") 80 | end 81 | 82 | if package:config("ubjson") then 83 | package:add("deps", "jsoncons") 84 | end 85 | 86 | local version = package:version() 87 | if version then 88 | if version:lt("0.13.0") then 89 | package:set("kind", "library", {headeronly = true}) 90 | end 91 | 92 | if version:ge("0.11.1") then 93 | package:add("deps", "ctre", {configs = {cmake = true}}) 94 | if version:eq("0.11.1") then 95 | package:add("defines", "REFLECTCPP_NO_BUNDLED_DEPENDENCIES") 96 | end 97 | end 98 | end 99 | 100 | if package:gitref() or version:lt("0.11.1") or version:ge("0.13.0") then 101 | package:add("deps", "cmake") 102 | end 103 | end) 104 | 105 | on_install(function (package) 106 | local version = package:version() 107 | if package:gitref() or version:lt("0.11.1") or version:ge("0.13.0") then 108 | local configs = { 109 | "-DREFLECTCPP_USE_BUNDLED_DEPENDENCIES=OFF", 110 | "-DREFLECTCPP_USE_VCPKG=OFF", 111 | } 112 | table.insert(configs, "-DCMAKE_BUILD_TYPE=" .. (package:is_debug() and "Debug" or "Release")) 113 | table.insert(configs, "-DBUILD_SHARED_LIBS=" .. (package:config("shared") and "ON" or "OFF")) 114 | table.insert(configs, "-DREFLECTCPP_BSON=" .. (package:config("bson") and "ON" or "OFF")) 115 | table.insert(configs, "-DREFLECTCPP_CBOR=" .. (package:config("cbor") and "ON" or "OFF")) 116 | table.insert(configs, "-DREFLECTCPP_FLEXBUFFERS=" .. (package:config("flatbuffers") and "ON" or "OFF")) 117 | table.insert(configs, "-DREFLECTCPP_MSGPACK=" .. (package:config("msgpack") and "ON" or "OFF")) 118 | table.insert(configs, "-DREFLECTCPP_XML=" .. (package:config("xml") and "ON" or "OFF")) 119 | table.insert(configs, "-DREFLECTCPP_TOML=" .. (package:config("toml") and "ON" or "OFF")) 120 | table.insert(configs, "-DREFLECTCPP_UBJSON=" .. (package:config("ubjson") and "ON" or "OFF")) 121 | table.insert(configs, "-DREFLECTCPP_YAML=" .. (package:config("yaml") and "ON" or "OFF")) 122 | import("package.tools.cmake").install(package, configs) 123 | 124 | os.rm("include/thirdparty") 125 | os.cp("include", package:installdir()) 126 | else 127 | os.rm("include/thirdparty") 128 | os.cp("include", package:installdir()) 129 | end 130 | end) 131 | 132 | on_test(function (package) 133 | assert(package:check_cxxsnippets({test = [[ 134 | #include 135 | #include 136 | struct Person { 137 | std::string first_name; 138 | std::string last_name; 139 | int age; 140 | }; 141 | const auto homer = Person{.first_name = "Homer", 142 | .last_name = "Simpson", 143 | .age = 45}; 144 | void test() { 145 | const std::string json_string = rfl::json::write(homer); 146 | auto homer2 = rfl::json::read(json_string).value(); 147 | } 148 | ]]}, {configs = {languages = "c++20"}})) 149 | if package:config("msgpack") then 150 | assert(package:check_cxxsnippets({test = [[ 151 | #include 152 | #include 153 | struct Person { 154 | std::string first_name; 155 | std::string last_name; 156 | int age; 157 | }; 158 | const auto homer = Person{.first_name = "Homer", 159 | .last_name = "Simpson", 160 | .age = 45}; 161 | void test() { 162 | std::vector msgpack_str_vec = rfl::msgpack::write(homer); 163 | auto homer2 = rfl::msgpack::read(msgpack_str_vec).value(); 164 | } 165 | ]]}, {configs = {languages = "c++20"}})) 166 | end 167 | end) -------------------------------------------------------------------------------- /resources/animated-preview1.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/std-microblock/breeze-shell/2cd7dda1029b6a414be7ff6ba723f10bf0a75fe4/resources/animated-preview1.webp -------------------------------------------------------------------------------- /resources/bloom.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/std-microblock/breeze-shell/2cd7dda1029b6a414be7ff6ba723f10bf0a75fe4/resources/bloom.webp -------------------------------------------------------------------------------- /resources/breeze.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | breeze-shell | MicroBlock 8 | 9 | 10 | 11 | 208 | 209 |
210 |
211 |
212 |
213 |
214 | breeze-shell 215 |
216 |
217 | Bring fluency & dedication back to Windows 218 |
219 | 220 |
221 |
222 | Download Now [密码: br] 223 |
224 | 225 |
226 | Download from Github 227 |
228 | 229 |
230 | Give me ⭐ 231 |
232 |
233 |
234 | 235 |
236 |
237 |
238 | Flexible 239 |
240 |
241 | Try once or inject consistently, it's your choice. 242 |
243 |
244 | 245 |
246 |
247 |
248 | 249 |
250 |
251 |
252 | Extensible 253 |
254 |
255 | Extend your context menu in a few lines of code. 256 |
257 |
258 |
259 |
260 |
261 | 262 |
263 |
264 |
265 | Fluent 266 |
267 |
268 | Beautiful and customizable UI with fluent animation. 269 |
270 |
271 |
272 |
273 |
274 | 275 | 283 |
284 | 285 | 305 | 306 | 307 | -------------------------------------------------------------------------------- /resources/code.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/std-microblock/breeze-shell/2cd7dda1029b6a414be7ff6ba723f10bf0a75fe4/resources/code.webp -------------------------------------------------------------------------------- /resources/icon-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/std-microblock/breeze-shell/2cd7dda1029b6a414be7ff6ba723f10bf0a75fe4/resources/icon-small.png -------------------------------------------------------------------------------- /resources/icon.7z: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/std-microblock/breeze-shell/2cd7dda1029b6a414be7ff6ba723f10bf0a75fe4/resources/icon.7z -------------------------------------------------------------------------------- /resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/std-microblock/breeze-shell/2cd7dda1029b6a414be7ff6ba723f10bf0a75fe4/resources/icon.png -------------------------------------------------------------------------------- /resources/icon.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/std-microblock/breeze-shell/2cd7dda1029b6a414be7ff6ba723f10bf0a75fe4/resources/icon.webp -------------------------------------------------------------------------------- /resources/inject-en.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/std-microblock/breeze-shell/2cd7dda1029b6a414be7ff6ba723f10bf0a75fe4/resources/inject-en.webp -------------------------------------------------------------------------------- /resources/inject.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/std-microblock/breeze-shell/2cd7dda1029b6a414be7ff6ba723f10bf0a75fe4/resources/inject.webp -------------------------------------------------------------------------------- /resources/preview1.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/std-microblock/breeze-shell/2cd7dda1029b6a414be7ff6ba723f10bf0a75fe4/resources/preview1.webp -------------------------------------------------------------------------------- /resources/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-06/schema#", 3 | "$ref": "#/definitions/Config", 4 | "definitions": { 5 | "Config": { 6 | "type": "object", 7 | "additionalProperties": false, 8 | "properties": { 9 | "context_menu": { 10 | "$ref": "#/definitions/ContextMenu" 11 | }, 12 | "debug_console": { 13 | "type": "boolean" 14 | }, 15 | "font_path_main": { 16 | "type": "string" 17 | }, 18 | "font_path_fallback": { 19 | "type": "string" 20 | }, 21 | "res_string_loader_use_hook": { 22 | "type": "boolean" 23 | }, 24 | "avoid_resize_ui": { 25 | "type": "boolean" 26 | }, 27 | "plugin_load_order": { 28 | "type": "array", 29 | "items": {} 30 | }, 31 | "$schema": { 32 | "type": "string" 33 | }, 34 | "default_animation": { 35 | "$ref": "#/definitions/AnimFloatConfig" 36 | } 37 | }, 38 | "required": [], 39 | "title": "Config" 40 | }, 41 | "ContextMenu": { 42 | "type": "object", 43 | "additionalProperties": false, 44 | "properties": { 45 | "theme": { 46 | "$ref": "#/definitions/Theme" 47 | }, 48 | "vsync": { 49 | "type": "boolean" 50 | }, 51 | "ignore_owner_draw": { 52 | "type": "boolean" 53 | }, 54 | "reverse_if_open_to_up": { 55 | "type": "boolean" 56 | }, 57 | "search_large_dwItemData_range": { 58 | "type": "boolean" 59 | }, 60 | "position": { 61 | "$ref": "#/definitions/Position" 62 | } 63 | }, 64 | "required": [], 65 | "title": "ContextMenu" 66 | }, 67 | "Position": { 68 | "type": "object", 69 | "additionalProperties": false, 70 | "properties": { 71 | "padding_vertical": { 72 | "type": "integer" 73 | }, 74 | "padding_horizontal": { 75 | "type": "integer" 76 | } 77 | }, 78 | "required": [], 79 | "title": "Position" 80 | }, 81 | "Theme": { 82 | "type": "object", 83 | "additionalProperties": false, 84 | "properties": { 85 | "use_dwm_if_available": { 86 | "type": "boolean" 87 | }, 88 | "background_opacity": { 89 | "type": "integer" 90 | }, 91 | "acrylic": { 92 | "type": "boolean" 93 | }, 94 | "radius": { 95 | "type": "integer" 96 | }, 97 | "font_size": { 98 | "type": "integer" 99 | }, 100 | "item_height": { 101 | "type": "integer" 102 | }, 103 | "item_gap": { 104 | "type": "integer" 105 | }, 106 | "item_radius": { 107 | "type": "integer" 108 | }, 109 | "margin": { 110 | "type": "integer" 111 | }, 112 | "padding": { 113 | "type": "integer" 114 | }, 115 | "text_padding": { 116 | "type": "integer" 117 | }, 118 | "icon_padding": { 119 | "type": "integer" 120 | }, 121 | "right_icon_padding": { 122 | "type": "integer" 123 | }, 124 | "multibutton_line_gap": { 125 | "type": "integer" 126 | }, 127 | "acrylic_color_light": { 128 | "type": "string" 129 | }, 130 | "acrylic_color_dark": { 131 | "type": "string" 132 | }, 133 | "acrylic_opacity": { 134 | "type": "number" 135 | }, 136 | "animation": { 137 | "$ref": "#/definitions/Animation" 138 | } 139 | }, 140 | "required": [], 141 | "title": "Theme" 142 | }, 143 | "Animation": { 144 | "type": "object", 145 | "additionalProperties": false, 146 | "properties": { 147 | "item": { 148 | "$ref": "#/definitions/Item" 149 | }, 150 | "main_bg": { 151 | "$ref": "#/definitions/Bg" 152 | }, 153 | "submenu_bg": { 154 | "$ref": "#/definitions/Bg" 155 | } 156 | }, 157 | "required": [], 158 | "title": "Animation" 159 | }, 160 | "Item": { 161 | "type": "object", 162 | "additionalProperties": false, 163 | "properties": { 164 | "opacity": { 165 | "$ref": "#/definitions/AnimFloatConfig" 166 | }, 167 | "x": { 168 | "$ref": "#/definitions/AnimFloatConfig" 169 | }, 170 | "y": { 171 | "$ref": "#/definitions/AnimFloatConfig" 172 | }, 173 | "width": { 174 | "$ref": "#/definitions/AnimFloatConfig" 175 | } 176 | }, 177 | "required": [], 178 | "title": "Item" 179 | }, 180 | "AnimFloatConfig": { 181 | "type": "object", 182 | "additionalProperties": false, 183 | "properties": { 184 | "duration": { 185 | "type": "integer" 186 | }, 187 | "easing": { 188 | "type": "string" 189 | }, 190 | "delay_scale": { 191 | "type": "integer" 192 | } 193 | }, 194 | "required": [], 195 | "title": "AnimFloatConfig" 196 | }, 197 | "Bg": { 198 | "type": "object", 199 | "additionalProperties": false, 200 | "properties": { 201 | "opacity": { 202 | "$ref": "#/definitions/AnimFloatConfig" 203 | }, 204 | "x": { 205 | "$ref": "#/definitions/AnimFloatConfig" 206 | }, 207 | "y": { 208 | "$ref": "#/definitions/AnimFloatConfig" 209 | }, 210 | "w": { 211 | "$ref": "#/definitions/AnimFloatConfig" 212 | }, 213 | "h": { 214 | "$ref": "#/definitions/AnimFloatConfig" 215 | } 216 | }, 217 | "required": [], 218 | "title": "Bg" 219 | } 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /scripts/bindgen/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /scripts/bindgen/c-type-parser.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface CTypeNode { 3 | function: boolean; 4 | template: boolean; 5 | type: string; 6 | argsTemplate: CTypeNode[]; 7 | argsFunc: CTypeNode[]; 8 | } 9 | 10 | export class CTypeParser { 11 | tokens: string[] = []; 12 | cursor = 0; 13 | 14 | lex(str: string) { 15 | this.tokens = [] 16 | let current = '' 17 | 18 | const BREAK_TOKENS = [' ', '(', ')', ',', '<', '>']; 19 | 20 | for (let i = 0; i < str.length; i++) { 21 | const c = str[i]; 22 | if (BREAK_TOKENS.includes(c)) { 23 | if (current.length > 0) { 24 | this.tokens.push(current); 25 | current = ''; 26 | } 27 | if (c !== ' ') { 28 | this.tokens.push(c); 29 | } 30 | } else { 31 | current += c; 32 | } 33 | } 34 | 35 | if (current.length > 0) { 36 | this.tokens.push(current); 37 | } 38 | } 39 | 40 | parse(str: string | null = null) { 41 | if (str) { 42 | this.lex(str) 43 | this.cursor = 0 44 | } 45 | 46 | const type: CTypeNode = { 47 | argsFunc: [], 48 | argsTemplate: [], 49 | function: false, 50 | template: false, 51 | type: '', 52 | } 53 | 54 | do { 55 | const parseCommaList = (arr) => { 56 | do { 57 | const res = this.parse(); 58 | if (res) 59 | arr.push(res) 60 | } while (this.eat(',')); 61 | } 62 | 63 | if (this.eat('(')) { 64 | type.function = true; 65 | if (!this.next(')')) 66 | parseCommaList(type.argsFunc); 67 | this.eat(')', true) 68 | } else if (this.eat('<')) { 69 | type.template = true; 70 | if (!this.next('>')) 71 | parseCommaList(type.argsTemplate) 72 | this.eat('>', true) 73 | } else { 74 | type.type = this.tokens[this.cursor] 75 | this.cursor++; 76 | } 77 | 78 | if (this.next('(') || this.next('<')) { 79 | continue; 80 | } 81 | 82 | break; 83 | } while (true); 84 | 85 | return type 86 | } 87 | 88 | eat(token, force = false) { 89 | if (this.next(token)) { 90 | this.cursor++; 91 | return true; 92 | } else { 93 | if (force) throw new Error(`Excepted: ${token}, found ${this.next()} in ${this.tokens.join(' ') 94 | }`) 95 | } 96 | return false 97 | } 98 | 99 | next(token: string | null = null) { 100 | if (this.tokens[this.cursor] === token || !token) { 101 | return this.tokens[this.cursor]; 102 | } 103 | return false 104 | } 105 | 106 | formatToC(node: CTypeNode) { 107 | let str = node.type; 108 | if (node.template) { 109 | str += '<' + node.argsTemplate.map(a => this.formatToC(a)).join(', ') + '>' 110 | } 111 | if (node.function) { 112 | str += '(' + node.argsFunc.map(a => this.formatToC(a)).join(', ') + ')' 113 | } 114 | return str 115 | } 116 | 117 | formatToTypeScript(node: CTypeNode) { 118 | node.type = node.type.split('::').pop() ?? node.type 119 | const typeMap = { 120 | 'int': 'number', 121 | 'float': 'number', 122 | 'double': 'number', 123 | 'string': 'string', 124 | 'vector': 'Array', 125 | 'bool': 'boolean', 126 | } 127 | 128 | let tsBasicType = (typeMap[node.type] ?? node.type) + (node.template ? '<' + node.argsTemplate.map(a => this.formatToTypeScript(a)).join(', ') + '>' : '') 129 | 130 | const ignoreTypes = ['variant', 'shared_ptr', 'function'] 131 | if ( 132 | ignoreTypes.includes(node.type) 133 | ) { 134 | tsBasicType = node.argsTemplate.map(a => this.formatToTypeScript(a)).join(' | ') 135 | } else if (node.type === 'optional') { 136 | tsBasicType = `${this.formatToTypeScript(node.argsTemplate[0])} | undefined` 137 | } 138 | 139 | if (node.function) { 140 | return `((${node.argsFunc.map(a => this.formatToTypeScript(a)).map((v, i) => `arg${i + 1}: ${v}`).join(', ')}) => ${tsBasicType})` 141 | } 142 | 143 | return tsBasicType; 144 | } 145 | } 146 | 147 | export const cTypeToTypeScript = (str: string) => { 148 | const parser = new CTypeParser(); 149 | parser.lex(str); 150 | const res = parser.parse() 151 | return parser.formatToTypeScript(res); 152 | } 153 | 154 | const parser = new CTypeParser(); 155 | 156 | const res = parser.parse('std::function(std::function)') 157 | console.log(JSON.stringify(res, null, 2), parser.formatToTypeScript(res)) -------------------------------------------------------------------------------- /scripts/bindgen/clang-ast.d.ts: -------------------------------------------------------------------------------- 1 | export interface inner { 2 | id?: string; 3 | kind?: string; 4 | loc?: loc; 5 | range?: range; 6 | inner?: inner[]; 7 | isImplicit?: boolean; 8 | name?: string; 9 | tagUsed?: string; 10 | implicit?: boolean; 11 | type?: type; 12 | decl?: decl; 13 | language?: string; 14 | hasBraces?: boolean; 15 | isReferenced?: boolean; 16 | previousDecl?: string; 17 | mangledName?: string; 18 | variadic?: boolean; 19 | inherited?: boolean; 20 | storageClass?: string; 21 | depth?: number; 22 | index?: number; 23 | completeDefinition?: boolean; 24 | definitionData?: definitionData; 25 | fixedUnderlyingType?: fixedUnderlyingType; 26 | valueCategory?: string; 27 | value?: boolean | string | number; 28 | isDependent?: boolean; 29 | isInstantiationDependent?: boolean; 30 | isPostfix?: boolean; 31 | opcode?: string; 32 | canOverflow?: boolean; 33 | isUsed?: boolean; 34 | ownedTagDecl?: ownedTagDecl; 35 | parentDeclContextId?: string; 36 | scopedEnumTag?: string; 37 | originalNamespace?: originalNamespace; 38 | constexpr?: boolean; 39 | explicitlyDefaulted?: string; 40 | inline?: boolean; 41 | visibility?: string; 42 | castKind?: string; 43 | referencedDecl?: referencedDecl; 44 | cc?: string; 45 | qualifiers?: string; 46 | access?: string; 47 | explicitlyDeleted?: boolean; 48 | message?: string; 49 | init?: string; 50 | isPartOfExplicitCast?: boolean; 51 | target?: target; 52 | argType?: argType; 53 | isArrow?: boolean; 54 | referencedMemberDecl?: string; 55 | templateName?: string; 56 | isExpr?: boolean; 57 | isAlias?: boolean; 58 | defaultArg?: defaultArg; 59 | bases?: bases[]; 60 | isParameterPack?: boolean; 61 | isPack?: boolean; 62 | containsUnexpandedPack?: boolean; 63 | usesADL?: boolean; 64 | lookups?: lookups[] | any[]; 65 | nonOdrUseReason?: string; 66 | foundReferencedDecl?: foundReferencedDecl; 67 | numElements?: number; 68 | hasElse?: boolean; 69 | computeLHSType?: computeLHSType; 70 | computeResultType?: computeResultType; 71 | storageDuration?: string; 72 | size?: number; 73 | qualifier?: string; 74 | isFunction?: boolean; 75 | const?: boolean; 76 | volatile?: boolean; 77 | refQualifier?: string; 78 | exceptionSpec?: string; 79 | isData?: boolean; 80 | transformKind?: string; 81 | member?: string; 82 | isConstexpr?: boolean; 83 | hasInClassInitializer?: boolean; 84 | list?: boolean; 85 | boundToLValueRef?: boolean; 86 | anyInit?: anyInit; 87 | ctorType?: ctorType; 88 | zeroing?: boolean; 89 | hadMultipleCandidates?: boolean; 90 | constructionKind?: string; 91 | virtual?: boolean; 92 | baseInit?: baseInit; 93 | path?: path[]; 94 | nrvo?: boolean; 95 | conversionFunc?: conversionFunc; 96 | cleanupsHaveSideEffects?: boolean; 97 | temp?: string; 98 | dtor?: dtor; 99 | isGlobal?: boolean; 100 | isPlacement?: boolean; 101 | isInline?: boolean; 102 | immediate?: boolean; 103 | adl?: boolean; 104 | ['inherited from']?: InheritedFrom; 105 | hasInit?: boolean; 106 | initStyle?: string; 107 | operatorNewDecl?: operatorNewDecl; 108 | operatorDeleteDecl?: operatorDeleteDecl; 109 | pack_index?: number; 110 | mutable?: boolean; 111 | pure?: boolean; 112 | array_filler?: array_filler[]; 113 | isInvalid?: boolean; 114 | } 115 | 116 | export interface loc { 117 | offset?: number; 118 | file?: string; 119 | line?: number; 120 | col?: number; 121 | tokLen?: number; 122 | includedFrom?: includedFrom; 123 | spellingLoc?: spellingLoc; 124 | expansionLoc?: expansionLoc; 125 | } 126 | 127 | export interface range { 128 | begin: begin; 129 | end: end; 130 | } 131 | 132 | export interface begin { 133 | offset?: number; 134 | col?: number; 135 | tokLen?: number; 136 | includedFrom?: includedFrom; 137 | line?: number; 138 | spellingLoc?: spellingLoc; 139 | expansionLoc?: expansionLoc; 140 | file?: string; 141 | } 142 | 143 | export interface end { 144 | offset?: number; 145 | line?: number; 146 | col?: number; 147 | tokLen?: number; 148 | includedFrom?: includedFrom; 149 | spellingLoc?: spellingLoc; 150 | expansionLoc?: expansionLoc; 151 | } 152 | 153 | export interface type { 154 | qualType: string; 155 | desugaredQualType?: string; 156 | typeAliasDeclId?: string; 157 | } 158 | 159 | export interface decl { 160 | id: string; 161 | kind?: string; 162 | name?: string; 163 | } 164 | 165 | export interface includedFrom { 166 | file: string; 167 | } 168 | 169 | export interface definitionData { 170 | canConstDefaultInit?: boolean; 171 | copyAssign: copyAssign; 172 | copyCtor: copyCtor; 173 | defaultCtor: defaultCtor; 174 | dtor: dtor; 175 | hasConstexprNonCopyMoveConstructor?: boolean; 176 | isAggregate?: boolean; 177 | isEmpty?: boolean; 178 | isLiteral?: boolean; 179 | isPOD?: boolean; 180 | isStandardLayout?: boolean; 181 | isTrivial?: boolean; 182 | isTriviallyCopyable?: boolean; 183 | moveAssign: moveAssign; 184 | moveCtor: moveCtor; 185 | canPassInRegisters?: boolean; 186 | hasUserDeclaredConstructor?: boolean; 187 | hasVariantMembers?: boolean; 188 | isPolymorphic?: boolean; 189 | hasMutableFields?: boolean; 190 | isGenericLambda?: boolean; 191 | isLambda?: boolean; 192 | isAbstract?: boolean; 193 | } 194 | 195 | export interface copyAssign { 196 | hasConstParam: boolean; 197 | implicitHasConstParam: boolean; 198 | needsImplicit?: boolean; 199 | simple?: boolean; 200 | trivial?: boolean; 201 | userDeclared?: boolean; 202 | needsOverloadResolution?: boolean; 203 | nonTrivial?: boolean; 204 | } 205 | 206 | export interface copyCtor { 207 | hasConstParam: boolean; 208 | implicitHasConstParam: boolean; 209 | needsImplicit?: boolean; 210 | simple?: boolean; 211 | trivial?: boolean; 212 | userDeclared?: boolean; 213 | needsOverloadResolution?: boolean; 214 | nonTrivial?: boolean; 215 | } 216 | 217 | export interface defaultCtor { 218 | defaultedIsConstexpr?: boolean; 219 | exists?: boolean; 220 | isConstexpr?: boolean; 221 | needsImplicit?: boolean; 222 | trivial?: boolean; 223 | nonTrivial?: boolean; 224 | userProvided?: boolean; 225 | } 226 | 227 | export interface dtor { 228 | irrelevant?: boolean; 229 | needsImplicit?: boolean; 230 | simple?: boolean; 231 | trivial?: boolean; 232 | nonTrivial?: boolean; 233 | userDeclared?: boolean; 234 | needsOverloadResolution?: boolean; 235 | id?: string; 236 | kind?: string; 237 | name?: string; 238 | type?: type; 239 | } 240 | 241 | export interface moveAssign { 242 | exists?: boolean; 243 | needsImplicit?: boolean; 244 | simple?: boolean; 245 | trivial?: boolean; 246 | userDeclared?: boolean; 247 | needsOverloadResolution?: boolean; 248 | nonTrivial?: boolean; 249 | } 250 | 251 | export interface moveCtor { 252 | exists?: boolean; 253 | needsImplicit?: boolean; 254 | simple?: boolean; 255 | trivial?: boolean; 256 | userDeclared?: boolean; 257 | needsOverloadResolution?: boolean; 258 | nonTrivial?: boolean; 259 | } 260 | 261 | export interface fixedUnderlyingType { 262 | qualType: string; 263 | desugaredQualType?: string; 264 | typeAliasDeclId?: string; 265 | } 266 | 267 | export interface spellingLoc { 268 | offset: number; 269 | file?: string; 270 | line?: number; 271 | col: number; 272 | tokLen: number; 273 | includedFrom?: includedFrom; 274 | presumedLine?: number; 275 | } 276 | 277 | export interface expansionLoc { 278 | offset: number; 279 | line?: number; 280 | col: number; 281 | tokLen: number; 282 | includedFrom: includedFrom; 283 | file?: string; 284 | isMacroArgExpansion?: boolean; 285 | } 286 | 287 | export interface ownedTagDecl { 288 | id: string; 289 | kind: string; 290 | name?: string; 291 | } 292 | 293 | export interface originalNamespace { 294 | id: string; 295 | kind: string; 296 | name: string; 297 | } 298 | 299 | export interface referencedDecl { 300 | id: string; 301 | kind: string; 302 | name?: string; 303 | type: type; 304 | } 305 | 306 | export interface target { 307 | id: string; 308 | kind: string; 309 | name: string; 310 | type?: type; 311 | } 312 | 313 | export interface argType { 314 | desugaredQualType?: string; 315 | qualType: string; 316 | typeAliasDeclId?: string; 317 | } 318 | 319 | export interface defaultArg { 320 | kind: string; 321 | type?: type; 322 | isExpr?: boolean; 323 | ['inherited from']?: InheritedFrom; 324 | } 325 | 326 | export interface bases { 327 | access: string; 328 | type: type; 329 | writtenAccess: string; 330 | } 331 | 332 | export interface lookups { 333 | id: string; 334 | kind: string; 335 | name: string; 336 | type?: type; 337 | } 338 | 339 | export interface foundReferencedDecl { 340 | id: string; 341 | kind: string; 342 | name: string; 343 | } 344 | 345 | export interface computeLHSType { 346 | qualType: string; 347 | desugaredQualType?: string; 348 | typeAliasDeclId?: string; 349 | } 350 | 351 | export interface computeResultType { 352 | qualType: string; 353 | desugaredQualType?: string; 354 | typeAliasDeclId?: string; 355 | } 356 | 357 | export interface anyInit { 358 | id: string; 359 | kind: string; 360 | name: string; 361 | type: type; 362 | } 363 | 364 | export interface ctorType { 365 | qualType: string; 366 | desugaredQualType?: string; 367 | } 368 | 369 | export interface baseInit { 370 | desugaredQualType?: string; 371 | qualType: string; 372 | typeAliasDeclId?: string; 373 | } 374 | 375 | export interface path { 376 | name: string; 377 | } 378 | 379 | export interface conversionFunc { 380 | id: string; 381 | kind: string; 382 | name: string; 383 | type: type; 384 | } 385 | 386 | export interface InheritedFrom { 387 | id: string; 388 | kind: string; 389 | name?: string; 390 | type?: type; 391 | } 392 | 393 | export interface operatorNewDecl { 394 | id: string; 395 | kind: string; 396 | name: string; 397 | type: type; 398 | } 399 | 400 | export interface operatorDeleteDecl { 401 | id: string; 402 | kind: string; 403 | name: string; 404 | type: type; 405 | } 406 | 407 | export interface array_filler { 408 | id: string; 409 | kind: string; 410 | range: range; 411 | type: type; 412 | valueCategory: string; 413 | } 414 | 415 | 416 | 417 | export type ClangASTD = inner; -------------------------------------------------------------------------------- /scripts/bindgen/generate-bindings.bat: -------------------------------------------------------------------------------- 1 | cd scripts 2 | cd bindgen 3 | clang++ -Xclang -ast-dump=json -fsyntax-only -Xclang -ast-dump-filter=mb_shell::js -std=c++2c ..\..\src\shell\script\binding_types.h > .\ast.json 4 | yarn && yarn gen 5 | echo Done -------------------------------------------------------------------------------- /scripts/bindgen/index.ts: -------------------------------------------------------------------------------- 1 | import { ClangASTD } from "./clang-ast"; 2 | import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs"; 3 | import { join } from "node:path"; 4 | import { cTypeToTypeScript } from "./c-type-parser"; 5 | 6 | const ast = JSON.parse(readFileSync(join(__dirname, "ast.json"), "utf-8")) as ClangASTD; 7 | 8 | const targetFile = 'binding_types.h' 9 | const outputFile = 'binding_qjs.h' 10 | // filter out loc.file contains targetFile 11 | 12 | const origFile = readFileSync(join(__dirname, '../../src/shell/script/binding_types.h'), 'utf-8').split('\n').map(v => v.trim()) 13 | 14 | let binding = 15 | `// This file is generated by scripts/bindgen/index.ts 16 | // Do not modify this file manually! 17 | 18 | #pragma once 19 | #include "binding_types.h" 20 | #include "quickjs.h" 21 | #include "quickjspp.hpp" 22 | 23 | template 24 | struct js_bind { 25 | static void bind(qjs::Context::Module &mod) {} 26 | }; 27 | ` 28 | 29 | let typescriptDef = `// This file is generated by scripts/bindgen/index.ts 30 | // Do not modify this file manually! 31 | 32 | declare module 'mshell' { 33 | 34 | ` 35 | 36 | const parseFunctionQualType = (type: string) => { 37 | // std::variant (std::string, std::string) 38 | // std::function (int, std::string) 39 | 40 | enum State { 41 | ReturnType, 42 | Args, 43 | Done 44 | } 45 | 46 | let state = State.ReturnType; 47 | let returnType = ''; 48 | let currentArg = ''; 49 | let args: string[] = []; 50 | let depth = 0; 51 | let angleBracketDepth = 0; 52 | 53 | for (let i = 0; i < type.length; i++) { 54 | const char = type[i]; 55 | 56 | if (char === '<') { 57 | angleBracketDepth++; 58 | } else if (char === '>') { 59 | angleBracketDepth--; 60 | } 61 | 62 | switch (state) { 63 | case State.ReturnType: 64 | if (char === '(' && angleBracketDepth === 0) { 65 | state = State.Args; 66 | returnType = returnType.trim(); 67 | } else { 68 | returnType += char; 69 | } 70 | break; 71 | 72 | case State.Args: 73 | if (char === '(') depth++; 74 | if (char === ')') { 75 | if (depth === 0 && angleBracketDepth === 0) { 76 | if (currentArg.trim()) args.push(currentArg.trim()); 77 | state = State.Done; 78 | break; 79 | } 80 | depth--; 81 | } 82 | if (char === ',' && depth === 0 && angleBracketDepth === 0) { 83 | args.push(currentArg.trim()); 84 | currentArg = ''; 85 | } else { 86 | currentArg += char; 87 | } 88 | break; 89 | } 90 | } 91 | 92 | if (state !== State.Done) { 93 | throw new Error('Invalid function type'); 94 | } 95 | 96 | return { 97 | returnType, 98 | args 99 | }; 100 | } 101 | 102 | if (ast.kind !== 'NamespaceDecl') { 103 | throw new Error('Root node is not a NamespaceDecl'); 104 | } 105 | 106 | let currentNamespace = 'mb_shell::js' 107 | 108 | const generateForRecordDecl = (node_struct: ClangASTD) => { 109 | if (node_struct.kind !== 'CXXRecordDecl') { 110 | throw new Error('Node is not a RecordDecl'); 111 | } 112 | 113 | const structName = node_struct.name; 114 | 115 | const fields: { 116 | name: string; 117 | type: string; 118 | comment?: string; 119 | }[] = []; 120 | 121 | const methods: { 122 | name: string; 123 | returnType: string; 124 | args: string[]; 125 | static: boolean; 126 | comment?: string; 127 | argNames?: string[]; 128 | }[] = []; 129 | 130 | if (!node_struct.inner) return; 131 | 132 | for (const node of node_struct.inner) { 133 | if (node.name?.startsWith('$')) continue; 134 | 135 | const lineNum = node.loc?.line; 136 | // find comment above 137 | let comment = ''; 138 | 139 | if (lineNum) { 140 | let rangeCommentCnt = 0; 141 | for (let i = lineNum - 2; i >= 0; i--) { 142 | const line = origFile[i]; 143 | if (!line) continue; 144 | if (line.startsWith('//')) { 145 | comment = line.substring(2) + '\n' + comment; 146 | continue; 147 | } 148 | if (line.startsWith('/*')) { 149 | rangeCommentCnt++; 150 | continue; 151 | } 152 | if (line.endsWith('*/')) { 153 | rangeCommentCnt--; 154 | continue; 155 | } 156 | 157 | if (rangeCommentCnt === 0) break; 158 | else comment = line + '\n' + comment; 159 | } 160 | } 161 | 162 | if (node.kind === 'FieldDecl') { 163 | fields.push({ 164 | name: node.name!, 165 | type: node.type!.qualType, 166 | comment: comment.length > 0 ? comment : undefined 167 | }); 168 | } 169 | 170 | if (node.kind === 'CXXMethodDecl') { 171 | const parsed = parseFunctionQualType(node.type!.qualType); 172 | 173 | if ( 174 | ['operator='].includes(node.name!) 175 | ) continue; 176 | 177 | const argNames: string[] = []; 178 | if (node.inner) { 179 | for (const arg of node.inner) { 180 | if (arg.kind === 'ParmVarDecl') { 181 | argNames.push(arg.name!); 182 | } 183 | } 184 | } 185 | 186 | methods.push({ 187 | name: node.name!, 188 | returnType: parsed.returnType, 189 | args: parsed.args, 190 | static: node.storageClass === 'static', 191 | comment: comment.length > 0 ? comment : undefined, 192 | argNames 193 | }); 194 | } 195 | } 196 | 197 | // console.log({ 198 | // structName, fields, methods 199 | // }); 200 | 201 | binding += ` 202 | template <> struct qjs::js_traits<${currentNamespace}::${structName}> { 203 | static ${currentNamespace}::${structName} unwrap(JSContext *ctx, JSValueConst v) { 204 | ${currentNamespace}::${structName} obj; 205 | `; 206 | 207 | for (const field of fields) { 208 | binding += ` 209 | obj.${field.name} = js_traits<${field.type}>::unwrap(ctx, JS_GetPropertyStr(ctx, v, "${field.name}")); 210 | `; 211 | } 212 | 213 | binding += ` 214 | return obj; 215 | } 216 | 217 | static JSValue wrap(JSContext *ctx, const ${currentNamespace}::${structName} &val) noexcept { 218 | JSValue obj = JS_NewObject(ctx); 219 | `; 220 | 221 | for (const field of fields) { 222 | binding += ` 223 | JS_SetPropertyStr(ctx, obj, "${field.name}", js_traits<${field.type}>::wrap(ctx, val.${field.name})); 224 | `; 225 | } 226 | 227 | binding += ` 228 | return obj; 229 | } 230 | };`; 231 | 232 | /** 233 | * 234 | * module.class_("MyClass") 235 | .constructor<>() 236 | .constructor>("MyClassA") 237 | .fun<&MyClass::member_variable>("member_variable") 238 | .fun<&MyClass::member_function>("member_function") 239 | .static_fun<&MyClass::static_function>("static_function") 240 | */ 241 | binding += ` 242 | template<> struct js_bind<${currentNamespace}::${structName}> { 243 | static void bind(qjs::Context::Module &mod) { 244 | mod.class_<${currentNamespace}::${structName}>("${structName}") 245 | .constructor<>()`; 246 | for (const method of methods) { 247 | if (method.static) { 248 | binding += ` 249 | .static_fun<&${currentNamespace}::${structName}::${method.name}>("${method.name}")`; 250 | } else { 251 | binding += ` 252 | .fun<&${currentNamespace}::${structName}::${method.name}>("${method.name}")`; 253 | } 254 | } 255 | 256 | for (const field of fields) { 257 | binding += ` 258 | .fun<&${currentNamespace}::${structName}::${field.name}>("${field.name}")`; 259 | } 260 | 261 | binding += ` 262 | ; 263 | } 264 | 265 | }; 266 | `; 267 | 268 | typescriptDef += ` 269 | export class ${structName} { 270 | \t${fields.map(field => { 271 | let fieldDef = `${field.name}${field.type.startsWith('std::optional') ? '?' : '' 272 | }: ${cTypeToTypeScript(field.type)}`; 273 | 274 | if (field.comment) { 275 | fieldDef = ` 276 | /** 277 | * ${field.comment.trim().split('\n').join('\n * ')} 278 | */ 279 | ${fieldDef} 280 | ` 281 | } 282 | 283 | return fieldDef; 284 | }).join('\n\t')} 285 | \t${methods.map(method => { 286 | let methodDef = `${method.static ? 'static ' : ''}${method.name}: ${cTypeToTypeScript(`${method.returnType}(${method.args.join(', ')})`)}` 287 | 288 | // if (method.comment) { 289 | // methodDef = method.comment.trim().split('\n').map(v => `// ${v}`).join('\n\t') 290 | // + '\n\t' + methodDef; 291 | // } 292 | 293 | // if (method.argNames) { 294 | // methodDef = '// Args: ' + method.argNames.join(', ') + '\n\t' + methodDef; 295 | // } 296 | let comments = ''; 297 | if (method.comment) { 298 | comments += method.comment.trim(); 299 | } 300 | methodDef = ` 301 | /** 302 | * ${comments.split('\n').join('\n * ')} 303 | * @param ${method.args.map((arg , i) => `${method.argNames![i]}: ${cTypeToTypeScript(arg)}`).join(', ')} 304 | * @returns ${cTypeToTypeScript(method.returnType)} 305 | */ 306 | ${methodDef} 307 | ` 308 | return methodDef; 309 | }).join('\n\t')} 310 | } 311 | `; 312 | 313 | } 314 | 315 | const structNames: string[] = [] 316 | for (const node of ast.inner!) { 317 | if (node.kind === 'CXXRecordDecl') { 318 | generateForRecordDecl(node); 319 | if (node.name && node.inner) 320 | structNames.push(node.name); 321 | } 322 | } 323 | 324 | binding += ` 325 | inline void bindAll(qjs::Context::Module &mod) { 326 | ` 327 | 328 | for (const structName of structNames) { 329 | binding += ` 330 | js_bind<${currentNamespace}::${structName}>::bind(mod); 331 | ` 332 | } 333 | 334 | binding += ` 335 | } 336 | ` 337 | 338 | typescriptDef += ` 339 | } 340 | ` 341 | 342 | const paths = [ 343 | join(__dirname, 'src/shell/script'), 344 | join(__dirname, '../src/shell/script'), 345 | join(__dirname, '../../src/shell/script') 346 | ] 347 | 348 | typescriptDef += readFileSync(join(__dirname, 'quickjs-types.txt'), 'utf-8'); 349 | 350 | for (const path of paths) { 351 | try { 352 | if (existsSync(join(path, targetFile))) { 353 | writeFileSync(join(path, outputFile), binding); 354 | writeFileSync(join(path, 'binding_types.d.ts'), typescriptDef); 355 | break; 356 | } 357 | } catch (e) { 358 | console.error(e); 359 | } 360 | } 361 | 362 | rmSync(join(__dirname, 'ast.json')); -------------------------------------------------------------------------------- /scripts/bindgen/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e", 3 | "name": "bindgen", 4 | "version": "1.0.0", 5 | "main": "index.js", 6 | "author": "MicroBlock <66859419+std-microblock@users.noreply.github.com>", 7 | "license": "MIT", 8 | "dependencies": { 9 | "@types/node": "^22.10.5", 10 | "esbuild": "^0.24.2", 11 | "esbuild-register": "^3.6.0" 12 | }, 13 | "scripts": { 14 | "gen": "node -r esbuild-register index.ts", 15 | "typegen": "node -r esbuild-register typegen.ts", 16 | "parser-test": "node -r esbuild-register c-type-parser.ts" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /scripts/bindgen/quickjs-types.txt: -------------------------------------------------------------------------------- 1 | declare module "mshell" { 2 | export function println(...args: any[]); 3 | type size_t = number; 4 | type uint8_t = number; 5 | type uint16_t = number; 6 | type uint32_t = number; 7 | type uint64_t = number; 8 | type int8_t = number; 9 | type int16_t = number; 10 | type int32_t = number; 11 | type int64_t = number; 12 | type intptr_t = number; 13 | type uintptr_t = number; 14 | type ssize_t = number; 15 | } 16 | 17 | // helper to access field based on dot path 18 | type FieldPath = K extends keyof T ? T[K] : K extends `${infer K1}.${infer K2}` ? K1 extends keyof T ? FieldPath : never : never; 19 | 20 | declare function plugin(import_meta: { url: string }, default_config?: T): { 21 | i18n: { 22 | define(lang: string, data: { [key: string]: string }): void; 23 | t(key: string): string; 24 | }; 25 | set_on_menu(callback: (m: 26 | import('mshell').menu_controller 27 | ) => void): void; 28 | config_directory: string; 29 | config: { 30 | read_config(): void; 31 | write_config(): void; 32 | get(key: K): FieldPath; 33 | set(key: K, value: FieldPath): void; 34 | all(): T; 35 | }; 36 | log(...args: any[]): void; 37 | }; 38 | 39 | declare type ShellPluginHelper = ReturnType; -------------------------------------------------------------------------------- /scripts/bindgen/typegen.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync, writeFileSync } from "fs"; 2 | import { types } from "util"; 3 | 4 | type Type = { 5 | isArr: boolean; 6 | type: 'string' | 'number' | 'boolean' | string; 7 | } 8 | type TypeMap = { 9 | [typeName: string]: { 10 | [fieldName: string]: { 11 | types: Type[]; 12 | isOptional: boolean; 13 | } 14 | }; 15 | }; 16 | 17 | function generateInterfaces(json: any): string { 18 | const typeMap: TypeMap = {}; 19 | 20 | const traverse = (obj: any, k = "Main") => { 21 | typeMap[k] ??= { 22 | }; 23 | 24 | const typeCur = typeMap[k]; 25 | 26 | for (const key in typeCur) { 27 | if (!obj[key]) { 28 | typeCur[key].isOptional = true; 29 | } 30 | } 31 | 32 | for (const key in obj) { 33 | const value = obj[key]; 34 | 35 | typeCur[key] ??= { 36 | types: [], 37 | isOptional: false 38 | }; 39 | 40 | const type = typeCur[key]; 41 | 42 | const pushIfNotExists = (t: Type) => { 43 | if (type.types.findIndex(x => x.type === t.type && x.isArr === t.isArr) === -1) { 44 | type.types.push(t); 45 | } 46 | } 47 | 48 | if (Array.isArray(value)) { 49 | if (value.length === 0) { 50 | pushIfNotExists({ 51 | isArr: true, 52 | type: 'any' 53 | }); 54 | } else { 55 | for (const val of value) { 56 | if (typeof val === 'object') { 57 | traverse(val, key); 58 | pushIfNotExists({ 59 | isArr: true, 60 | type: key 61 | }); 62 | } else { 63 | pushIfNotExists({ 64 | isArr: true, 65 | type: typeof val 66 | }); 67 | } 68 | } 69 | } 70 | } else if (typeof value === 'object') { 71 | pushIfNotExists({ 72 | isArr: false, 73 | type: key 74 | }); 75 | traverse(value, key); 76 | } else { 77 | pushIfNotExists({ 78 | isArr: false, 79 | type: typeof value 80 | }); 81 | } 82 | } 83 | } 84 | 85 | traverse(json); 86 | 87 | let ts = ''; 88 | 89 | const escapeForType = (type: string) => { 90 | if (!type.includes(' ')) return type; 91 | return type.split(' ').map(x => x[0].toUpperCase() + x.slice(1)).join(''); 92 | } 93 | 94 | const escapeForField = (field: string) => { 95 | const keywords = ['delete', 'export', 'import', 'in', 'instanceof', 'new', 'typeof', 'void', 'yield']; 96 | if (keywords.includes(field) || field.includes('-') || field.includes(' ')) { 97 | return `['${field}']`; 98 | } 99 | 100 | return field; 101 | } 102 | 103 | for (const type in typeMap) { 104 | ts += `export interface ${escapeForType(type)} {\n`; 105 | 106 | for (const field in typeMap[type]) { 107 | const { types, isOptional } = typeMap[type][field]; 108 | 109 | ts += ` ${escapeForField(field)}${isOptional ? '?' : ''}: ${types.map(x => `${escapeForType(x.type)}${x.isArr ? '[]' : ''}`).join(' | ')};\n`; 110 | } 111 | 112 | ts += '}\n\n'; 113 | } 114 | 115 | return ts; 116 | } 117 | 118 | const json = JSON.parse( 119 | readFileSync('ast.json', 'utf-8') 120 | ) 121 | 122 | const ts = generateInterfaces({ 123 | inner: json 124 | }); 125 | 126 | writeFileSync('ast.d.ts', ts); -------------------------------------------------------------------------------- /scripts/bindgen/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@esbuild/aix-ppc64@0.24.2": 6 | version "0.24.2" 7 | resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz#38848d3e25afe842a7943643cbcd387cc6e13461" 8 | integrity sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA== 9 | 10 | "@esbuild/android-arm64@0.24.2": 11 | version "0.24.2" 12 | resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz#f592957ae8b5643129fa889c79e69cd8669bb894" 13 | integrity sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg== 14 | 15 | "@esbuild/android-arm@0.24.2": 16 | version "0.24.2" 17 | resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.24.2.tgz#72d8a2063aa630308af486a7e5cbcd1e134335b3" 18 | integrity sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q== 19 | 20 | "@esbuild/android-x64@0.24.2": 21 | version "0.24.2" 22 | resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.24.2.tgz#9a7713504d5f04792f33be9c197a882b2d88febb" 23 | integrity sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw== 24 | 25 | "@esbuild/darwin-arm64@0.24.2": 26 | version "0.24.2" 27 | resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz#02ae04ad8ebffd6e2ea096181b3366816b2b5936" 28 | integrity sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA== 29 | 30 | "@esbuild/darwin-x64@0.24.2": 31 | version "0.24.2" 32 | resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz#9ec312bc29c60e1b6cecadc82bd504d8adaa19e9" 33 | integrity sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA== 34 | 35 | "@esbuild/freebsd-arm64@0.24.2": 36 | version "0.24.2" 37 | resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz#5e82f44cb4906d6aebf24497d6a068cfc152fa00" 38 | integrity sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg== 39 | 40 | "@esbuild/freebsd-x64@0.24.2": 41 | version "0.24.2" 42 | resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz#3fb1ce92f276168b75074b4e51aa0d8141ecce7f" 43 | integrity sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q== 44 | 45 | "@esbuild/linux-arm64@0.24.2": 46 | version "0.24.2" 47 | resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz#856b632d79eb80aec0864381efd29de8fd0b1f43" 48 | integrity sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg== 49 | 50 | "@esbuild/linux-arm@0.24.2": 51 | version "0.24.2" 52 | resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz#c846b4694dc5a75d1444f52257ccc5659021b736" 53 | integrity sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA== 54 | 55 | "@esbuild/linux-ia32@0.24.2": 56 | version "0.24.2" 57 | resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz#f8a16615a78826ccbb6566fab9a9606cfd4a37d5" 58 | integrity sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw== 59 | 60 | "@esbuild/linux-loong64@0.24.2": 61 | version "0.24.2" 62 | resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz#1c451538c765bf14913512c76ed8a351e18b09fc" 63 | integrity sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ== 64 | 65 | "@esbuild/linux-mips64el@0.24.2": 66 | version "0.24.2" 67 | resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz#0846edeefbc3d8d50645c51869cc64401d9239cb" 68 | integrity sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw== 69 | 70 | "@esbuild/linux-ppc64@0.24.2": 71 | version "0.24.2" 72 | resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz#8e3fc54505671d193337a36dfd4c1a23b8a41412" 73 | integrity sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw== 74 | 75 | "@esbuild/linux-riscv64@0.24.2": 76 | version "0.24.2" 77 | resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz#6a1e92096d5e68f7bb10a0d64bb5b6d1daf9a694" 78 | integrity sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q== 79 | 80 | "@esbuild/linux-s390x@0.24.2": 81 | version "0.24.2" 82 | resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz#ab18e56e66f7a3c49cb97d337cd0a6fea28a8577" 83 | integrity sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw== 84 | 85 | "@esbuild/linux-x64@0.24.2": 86 | version "0.24.2" 87 | resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz#8140c9b40da634d380b0b29c837a0b4267aff38f" 88 | integrity sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q== 89 | 90 | "@esbuild/netbsd-arm64@0.24.2": 91 | version "0.24.2" 92 | resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz#65f19161432bafb3981f5f20a7ff45abb2e708e6" 93 | integrity sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw== 94 | 95 | "@esbuild/netbsd-x64@0.24.2": 96 | version "0.24.2" 97 | resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz#7a3a97d77abfd11765a72f1c6f9b18f5396bcc40" 98 | integrity sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw== 99 | 100 | "@esbuild/openbsd-arm64@0.24.2": 101 | version "0.24.2" 102 | resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz#58b00238dd8f123bfff68d3acc53a6ee369af89f" 103 | integrity sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A== 104 | 105 | "@esbuild/openbsd-x64@0.24.2": 106 | version "0.24.2" 107 | resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz#0ac843fda0feb85a93e288842936c21a00a8a205" 108 | integrity sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA== 109 | 110 | "@esbuild/sunos-x64@0.24.2": 111 | version "0.24.2" 112 | resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz#8b7aa895e07828d36c422a4404cc2ecf27fb15c6" 113 | integrity sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig== 114 | 115 | "@esbuild/win32-arm64@0.24.2": 116 | version "0.24.2" 117 | resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz#c023afb647cabf0c3ed13f0eddfc4f1d61c66a85" 118 | integrity sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ== 119 | 120 | "@esbuild/win32-ia32@0.24.2": 121 | version "0.24.2" 122 | resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz#96c356132d2dda990098c8b8b951209c3cd743c2" 123 | integrity sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA== 124 | 125 | "@esbuild/win32-x64@0.24.2": 126 | version "0.24.2" 127 | resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz#34aa0b52d0fbb1a654b596acfa595f0c7b77a77b" 128 | integrity sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg== 129 | 130 | "@types/node@^22.10.5": 131 | version "22.10.5" 132 | resolved "https://registry.yarnpkg.com/@types/node/-/node-22.10.5.tgz#95af89a3fb74a2bb41ef9927f206e6472026e48b" 133 | integrity sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ== 134 | dependencies: 135 | undici-types "~6.20.0" 136 | 137 | debug@^4.3.4: 138 | version "4.4.0" 139 | resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a" 140 | integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA== 141 | dependencies: 142 | ms "^2.1.3" 143 | 144 | esbuild-register@^3.6.0: 145 | version "3.6.0" 146 | resolved "https://registry.yarnpkg.com/esbuild-register/-/esbuild-register-3.6.0.tgz#cf270cfa677baebbc0010ac024b823cbf723a36d" 147 | integrity sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg== 148 | dependencies: 149 | debug "^4.3.4" 150 | 151 | esbuild@^0.24.2: 152 | version "0.24.2" 153 | resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.24.2.tgz#b5b55bee7de017bff5fb8a4e3e44f2ebe2c3567d" 154 | integrity sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA== 155 | optionalDependencies: 156 | "@esbuild/aix-ppc64" "0.24.2" 157 | "@esbuild/android-arm" "0.24.2" 158 | "@esbuild/android-arm64" "0.24.2" 159 | "@esbuild/android-x64" "0.24.2" 160 | "@esbuild/darwin-arm64" "0.24.2" 161 | "@esbuild/darwin-x64" "0.24.2" 162 | "@esbuild/freebsd-arm64" "0.24.2" 163 | "@esbuild/freebsd-x64" "0.24.2" 164 | "@esbuild/linux-arm" "0.24.2" 165 | "@esbuild/linux-arm64" "0.24.2" 166 | "@esbuild/linux-ia32" "0.24.2" 167 | "@esbuild/linux-loong64" "0.24.2" 168 | "@esbuild/linux-mips64el" "0.24.2" 169 | "@esbuild/linux-ppc64" "0.24.2" 170 | "@esbuild/linux-riscv64" "0.24.2" 171 | "@esbuild/linux-s390x" "0.24.2" 172 | "@esbuild/linux-x64" "0.24.2" 173 | "@esbuild/netbsd-arm64" "0.24.2" 174 | "@esbuild/netbsd-x64" "0.24.2" 175 | "@esbuild/openbsd-arm64" "0.24.2" 176 | "@esbuild/openbsd-x64" "0.24.2" 177 | "@esbuild/sunos-x64" "0.24.2" 178 | "@esbuild/win32-arm64" "0.24.2" 179 | "@esbuild/win32-ia32" "0.24.2" 180 | "@esbuild/win32-x64" "0.24.2" 181 | 182 | ms@^2.1.3: 183 | version "2.1.3" 184 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" 185 | integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== 186 | 187 | undici-types@~6.20.0: 188 | version "6.20.0" 189 | resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.20.0.tgz#8171bf22c1f588d1554d55bf204bc624af388433" 190 | integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg== 191 | -------------------------------------------------------------------------------- /scripts/debug.cmd: -------------------------------------------------------------------------------- 1 | set LLDB_USE_NATIVE_PDB_READER=1 2 | xmake b shell_test 3 | xmake r -d shell_test -------------------------------------------------------------------------------- /scripts/rebuild.ps1: -------------------------------------------------------------------------------- 1 | $pids = (Get-WmiObject Win32_Process -Filter "name = 'explorer.exe'" | where { $_.CommandLine -like '*/factory,{75dff2b7-6936-4c06-a8bb-676a7b00b24b}*' }).ProcessId 2 | foreach ($pidx in $pids) { 3 | Stop-Process -Id $pidx -Force 4 | } 5 | 6 | xmake b --yes inject 7 | xmake b --yes shell && xmake r inject new -------------------------------------------------------------------------------- /scripts/right_click.ahk: -------------------------------------------------------------------------------- 1 | Sleep, 1000 2 | counter := 0 3 | #SingleInstance force 4 | 5 | Gui, Add, Edit, vLog w400 h400, 6 | Gui, Show, w400 h400 7 | stop := false 8 | 9 | Loop 10 | { 11 | Send {RButton} 12 | Sleep, 100 13 | Send {Esc} 14 | Sleep, 100 15 | counter := counter + 1 16 | GuiControl,, Log, %counter% 17 | 18 | if (stop) 19 | { 20 | break 21 | } 22 | } 23 | 24 | F10:: 25 | stop := true 26 | -------------------------------------------------------------------------------- /src/inject/data_directory.inc: -------------------------------------------------------------------------------- 1 | std::filesystem::path data_directory() { 2 | static std::optional path; 3 | static std::mutex mtx; 4 | std::lock_guard lock(mtx); 5 | 6 | if (!path) { 7 | wchar_t home_dir[MAX_PATH]; 8 | GetEnvironmentVariableW(L"USERPROFILE", home_dir, MAX_PATH); 9 | path = std::filesystem::path(home_dir) / ".breeze-shell"; 10 | } 11 | 12 | if (!std::filesystem::exists(*path)) { 13 | std::filesystem::create_directories(*path); 14 | } 15 | 16 | return path.value(); 17 | } -------------------------------------------------------------------------------- /src/shell/build_info.h.in: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #define BREEZE_VERSION "${VERSION}" 3 | #define BREEZE_VERSION_MAJOR ${VERSION_MAJOR} 4 | #define BREEZE_VERSION_MINOR ${VERSION_MINOR} 5 | #define BREEZE_GIT_COMMIT_HASH "${GIT_COMMIT_HASH}" 6 | #define BREEZE_GIT_BRANCH_NAME "${GIT_BRANCH_NAME}" 7 | #define BREEZE_BUILD_DATE_TIME "${BUILD_DATE_TIME}" -------------------------------------------------------------------------------- /src/shell/config.cc: -------------------------------------------------------------------------------- 1 | #include "config.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "logger.h" 9 | #include "rfl.hpp" 10 | #include "rfl/DefaultIfMissing.hpp" 11 | #include "rfl/json.hpp" 12 | 13 | #include "utils.h" 14 | #include "windows.h" 15 | 16 | namespace mb_shell { 17 | std::unique_ptr config::current; 18 | config::animated_float_conf config::_default_animation{ 19 | .duration = 150, 20 | .easing = ui::easing_type::ease_in_out, 21 | .delay_scale = 1, 22 | }; 23 | 24 | void config::write_config() { 25 | auto config_file = data_directory() / "config.json"; 26 | std::ofstream ofs(config_file); 27 | if (!ofs) { 28 | std::cerr << "Failed to write config file." << std::endl; 29 | return; 30 | } 31 | 32 | ofs << rfl::json::write(*config::current); 33 | } 34 | void config::read_config() { 35 | auto config_file = data_directory() / "config.json"; 36 | 37 | #ifdef __llvm__ 38 | std::ifstream ifs(config_file); 39 | if (!std::filesystem::exists(config_file)) { 40 | auto config_file = data_directory() / "config.json"; 41 | std::ofstream ofs(config_file); 42 | if (!ofs) { 43 | std::cerr << "Failed to write config file." << std::endl; 44 | } 45 | 46 | ofs << R"({ 47 | "$schema": "https://raw.githubusercontent.com/std-microblock/breeze-shell/refs/heads/master/resources/schema.json" 48 | })"; 49 | } 50 | if (!ifs) { 51 | std::cerr 52 | << "Config file could not be opened. Using default config instead." 53 | << std::endl; 54 | config::current = std::make_unique(); 55 | config::current->debug_console = true; 56 | } else { 57 | std::string json_str; 58 | std::copy(std::istreambuf_iterator(ifs), 59 | std::istreambuf_iterator(), std::back_inserter(json_str)); 60 | 61 | if (auto json = 62 | rfl::json::read( 63 | json_str)) { 64 | // parse twice for default value 65 | _default_animation = json.value().default_animation; 66 | json = rfl::json::read( 67 | json_str); 68 | config::current = std::make_unique(json.value()); 69 | std::cout << "Config reloaded." << std::endl; 70 | } else { 71 | std::cerr << "Failed to read config file: " << json.error()->what() 72 | << "\nUsing default config instead." << std::endl; 73 | config::current = std::make_unique(); 74 | config::current->debug_console = true; 75 | } 76 | } 77 | #else 78 | #pragma message \ 79 | "We don't support loading config file on MSVC because of a bug in MSVC." 80 | dbgout("We don't support loading config file when compiled with MSVC " 81 | "because of a bug in MSVC."); 82 | config::current = std::make_unique(); 83 | config::current->debug_console = true; 84 | #endif 85 | 86 | if (config::current->debug_console) { 87 | ShowWindow(GetConsoleWindow(), SW_SHOW); 88 | } else { 89 | ShowWindow(GetConsoleWindow(), SW_HIDE); 90 | } 91 | } 92 | 93 | std::filesystem::path config::data_directory() { 94 | static std::optional path; 95 | static std::mutex mtx; 96 | std::lock_guard lock(mtx); 97 | 98 | if (!path) { 99 | path = std::filesystem::path(env("USERPROFILE").value()) / ".breeze-shell"; 100 | } 101 | 102 | if (!std::filesystem::exists(*path)) { 103 | std::filesystem::create_directories(*path); 104 | } 105 | 106 | return path.value(); 107 | } 108 | void config::run_config_loader() { 109 | auto config_path = config::data_directory() / "config.json"; 110 | dbgout("config file: {}", config_path.string()); 111 | config::read_config(); 112 | std::thread([config_path]() { 113 | auto last_mod = std::filesystem::last_write_time(config_path); 114 | while (true) { 115 | if (std::filesystem::last_write_time(config_path) != last_mod) { 116 | last_mod = std::filesystem::last_write_time(config_path); 117 | config::read_config(); 118 | } 119 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 120 | } 121 | }).detach(); 122 | } 123 | void config::animated_float_conf::apply_to(ui::sp_anim_float &anim, 124 | float delay) { 125 | anim->set_duration(duration); 126 | anim->set_easing(easing); 127 | anim->set_delay(delay * delay_scale); 128 | } 129 | void config::animated_float_conf::operator()(ui::sp_anim_float &anim, 130 | float delay) { 131 | apply_to(anim, delay); 132 | } 133 | 134 | std::filesystem::path config::default_main_font() { 135 | return std::filesystem::path(env("WINDIR").value()) / "Fonts" / "segoeui.ttf"; 136 | } 137 | std::filesystem::path config::default_fallback_font() { 138 | return std::filesystem::path(env("WINDIR").value()) / "Fonts" / "msyh.ttc"; 139 | } 140 | std::string config::dump_config() { return rfl::json::write(*config::current); } 141 | } // namespace mb_shell -------------------------------------------------------------------------------- /src/shell/config.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "animator.h" 4 | #include 5 | #include 6 | #include 7 | namespace mb_shell { 8 | 9 | struct config { 10 | static std::filesystem::path default_main_font(); 11 | static std::filesystem::path default_fallback_font(); 12 | 13 | struct animated_float_conf { 14 | float duration = _default_animation.duration; 15 | ui::easing_type easing = _default_animation.easing; 16 | float delay_scale = _default_animation.delay_scale; 17 | 18 | void apply_to(ui::sp_anim_float &anim, float delay = 0); 19 | void operator()(ui::sp_anim_float &anim, float delay = 0); 20 | } default_animation; 21 | static animated_float_conf _default_animation; 22 | struct context_menu { 23 | struct theme { 24 | bool use_dwm_if_available = true; 25 | float background_opacity = 1; 26 | bool acrylic = true; 27 | float radius = 6; 28 | float font_size = 14; 29 | float item_height = 23; 30 | float item_gap = 3; 31 | float item_radius = 5; 32 | float margin = 5; 33 | float padding = 6; 34 | float text_padding = 8; 35 | float icon_padding = 4; 36 | float right_icon_padding = 20; 37 | float multibutton_line_gap = -6; 38 | 39 | std::string acrylic_color_light = "#fefefe00"; 40 | std::string acrylic_color_dark = "#28282800"; 41 | 42 | // unused, only for backward compatibility 43 | float acrylic_opacity = 0.1; 44 | 45 | struct animation { 46 | struct main { 47 | animated_float_conf y; 48 | } main; 49 | struct item { 50 | animated_float_conf opacity; 51 | animated_float_conf x, y; 52 | animated_float_conf width; 53 | } item; 54 | struct bg { 55 | animated_float_conf opacity; 56 | animated_float_conf x, y, w, h; 57 | } main_bg, submenu_bg; 58 | } animation; 59 | } theme; 60 | 61 | bool vsync = true; 62 | bool ignore_owner_draw = true; 63 | bool reverse_if_open_to_up = true; 64 | 65 | // debug purpose only 66 | bool search_large_dwItemData_range = false; 67 | 68 | struct position { 69 | int padding_vertical = 20; 70 | int padding_horizontal = 0; 71 | } position; 72 | } context_menu; 73 | bool debug_console = false; 74 | // Restart to apply font/hook changes 75 | std::filesystem::path font_path_main = default_main_font(); 76 | std::filesystem::path font_path_fallback = default_fallback_font(); 77 | bool res_string_loader_use_hook = false; 78 | bool avoid_resize_ui = false; 79 | std::vector plugin_load_order = {}; 80 | 81 | std::string $schema; 82 | static std::unique_ptr current; 83 | static void read_config(); 84 | static void write_config(); 85 | static void run_config_loader(); 86 | static std::string dump_config(); 87 | 88 | static std::filesystem::path data_directory(); 89 | }; 90 | } // namespace mb_shell -------------------------------------------------------------------------------- /src/shell/contextmenu/contextmenu.cc: -------------------------------------------------------------------------------- 1 | 2 | #include "contextmenu.h" 3 | #include "../utils.h" 4 | #include "menu_widget.h" 5 | #include "ui.h" 6 | #include 7 | #include 8 | 9 | #include "menu_render.h" 10 | 11 | #include "../config.h" 12 | #include "../res_string_loader.h" 13 | 14 | #include "../logger.h" 15 | 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | 27 | namespace mb_shell { 28 | owner_draw_menu_info getBitmapFromOwnerDraw(MENUITEMINFOW *menuItemInfo, 29 | HWND hwnd) { 30 | owner_draw_menu_info result = {{}, 0, 0}; 31 | MEASUREITEMSTRUCT measureItem = {0}; 32 | measureItem.CtlType = ODT_MENU; 33 | measureItem.CtlID = 0; 34 | measureItem.itemID = menuItemInfo->wID; 35 | measureItem.itemData = (ULONG_PTR)(menuItemInfo->dwItemData); 36 | 37 | SendMessageW(hwnd, WM_MEASUREITEM, 0, 38 | reinterpret_cast(&measureItem)); 39 | 40 | result.width = measureItem.itemWidth; 41 | result.height = measureItem.itemHeight; 42 | 43 | if (result.width == 0 || result.height == 0) { 44 | return result; 45 | } 46 | 47 | HDC hdc = GetDC(hwnd); 48 | if (!hdc) 49 | return result; 50 | 51 | HDC memDC = CreateCompatibleDC(hdc); 52 | if (!memDC) { 53 | ReleaseDC(hwnd, hdc); 54 | return result; 55 | } 56 | 57 | RECT rcItem = {0, 0, static_cast(result.width), 58 | static_cast(result.height)}; 59 | FillRect(memDC, &rcItem, (HBRUSH)GetStockObject(WHITE_BRUSH)); 60 | 61 | DRAWITEMSTRUCT drawItem = {0}; 62 | drawItem.CtlType = ODT_MENU; 63 | drawItem.CtlID = 0; 64 | drawItem.itemID = menuItemInfo->wID; 65 | drawItem.itemAction = ODA_DRAWENTIRE; 66 | drawItem.itemState = 0; 67 | drawItem.hwndItem = (HWND)menuItemInfo->hSubMenu; 68 | drawItem.hDC = memDC; 69 | drawItem.rcItem = rcItem; 70 | drawItem.itemData = (ULONG_PTR)(menuItemInfo->dwItemData); 71 | 72 | SendMessageW(hwnd, WM_DRAWITEM, 0, 73 | reinterpret_cast(&drawItem)); // 发送绘制消息 74 | 75 | result.bitmap = CreateCompatibleBitmap(hdc, result.width, result.height); 76 | if (!result.bitmap) { 77 | DeleteDC(memDC); 78 | ReleaseDC(hwnd, hdc); 79 | return result; 80 | } 81 | return result; 82 | } 83 | 84 | std::wstring strip_extra_infos(std::wstring_view str) { 85 | // 1. Test&C -> Test 86 | // 2. Test -> Test 87 | // 3. Test\txxx -> Test 88 | // 4. remove all unicode control characters 89 | 90 | std::wstring result; 91 | 92 | for (int i = 0; i < str.size(); i++) { 93 | if (str[i] == '(' && i + 1 < str.size() && str[i + 1] == '&') { 94 | while (str[i] != ')' && i < str.size()) { 95 | i++; 96 | } 97 | 98 | continue; 99 | } 100 | 101 | if (str[i] == '&') { 102 | continue; 103 | } 104 | 105 | if (str[i] == '\t') { 106 | break; 107 | } 108 | result.push_back(str[i]); 109 | } 110 | 111 | return result; 112 | } 113 | 114 | menu menu::construct_with_hmenu(HMENU hMenu, HWND hWnd, bool is_top) { 115 | menu m; 116 | 117 | SendMessageW(hWnd, WM_INITMENUPOPUP, reinterpret_cast(hMenu), 118 | 0xFFFFFFFF); 119 | for (int i = 0; i < GetMenuItemCount(hMenu); i++) { 120 | menu_item item; 121 | wchar_t buffer[256]; 122 | MENUITEMINFOW info = {sizeof(MENUITEMINFO)}; 123 | info.fMask = MIIM_STRING | MIIM_SUBMENU | MIIM_ID | MIIM_FTYPE | 124 | MIIM_STATE | MIIM_BITMAP | MIIM_CHECKMARKS | MIIM_DATA; 125 | info.dwTypeData = buffer; 126 | info.cch = 256; 127 | if (!GetMenuItemInfoW(hMenu, i, TRUE, &info)) { 128 | std::cout << "Failed to get menu item info: " << GetLastError() 129 | << std::endl; 130 | continue; 131 | } 132 | 133 | if (info.fType & MFT_OWNERDRAW) { 134 | auto od = getBitmapFromOwnerDraw(&info, hWnd); 135 | if (od.width && od.height) { 136 | item.owner_draw = od; 137 | } 138 | } 139 | 140 | if (info.fType & MFT_RADIOCHECK || info.fState & MFS_CHECKED) { 141 | auto c = is_light_mode() ? 0 : 1; 142 | if ((!item.icon_bitmap && !item.icon_svg)) { 143 | item.icon_svg = std::format( 144 | R"#()#", 145 | c ? "white" : "black"); 146 | } 147 | } 148 | 149 | if (info.hSubMenu) { 150 | PostMessageW(hWnd, WM_INITMENUPOPUP, 151 | reinterpret_cast(info.hSubMenu), 0xFFFFFFFF); 152 | item.submenu = 153 | [data = menu::construct_with_hmenu(info.hSubMenu, hWnd, false)]( 154 | std::shared_ptr mw) { mw->init_from_data(data); }; 155 | } else { 156 | item.action = [=]() mutable { 157 | menu_render::current.value()->selected_menu = info.wID; 158 | menu_render::current.value()->rt->hide_as_close(); 159 | }; 160 | 161 | item.wID = info.wID; 162 | } 163 | 164 | if (info.fType & MFT_SEPARATOR || info.fType & MFT_MENUBARBREAK || 165 | info.fType & MFT_MENUBREAK) { 166 | item.type = menu_item::type::spacer; 167 | } else { 168 | item.name = wstring_to_utf8(strip_extra_infos(buffer)); 169 | auto id_stripped = res_string_loader::string_to_id(buffer); 170 | if (std::get_if(&id_stripped)) { 171 | item.name_resid = 172 | res_string_loader::string_to_id_string(strip_extra_infos(buffer)); 173 | } else { 174 | item.name_resid = res_string_loader::string_to_id_string(buffer); 175 | } 176 | } 177 | 178 | if (info.fType & MFT_BITMAP) { 179 | item.icon_bitmap = (size_t)info.hbmpItem; 180 | } else if (info.hbmpChecked || info.hbmpUnchecked) { 181 | if (info.fState & MFS_CHECKED) 182 | item.icon_bitmap = (size_t)info.hbmpChecked; 183 | else 184 | item.icon_bitmap = (size_t)info.hbmpUnchecked; 185 | } 186 | 187 | if (info.dwItemData) { 188 | HBITMAP result{}; 189 | 190 | if (!IS_INTRESOURCE(info.dwItemData)) { 191 | auto offsets = 192 | config::current->context_menu.search_large_dwItemData_range 193 | ? ([]() -> std::vector { 194 | std::vector offsets; 195 | for (int i = 0; i <= 0xfff; i++) { 196 | offsets.push_back(i); 197 | } 198 | return offsets; 199 | }()) 200 | : std::vector{0x018, 0x220, 0x020, 0x000}; 201 | for (int offset : offsets) { 202 | auto pHBitmap = reinterpret_cast(info.dwItemData + offset); 203 | if (!is_memory_readable(pHBitmap)) { 204 | continue; 205 | } 206 | 207 | result = *pHBitmap; 208 | if (result && ::GetObjectType(result) == OBJ_BITMAP) { 209 | BITMAP bitmap{}; 210 | if (::GetObjectW(result, sizeof(BITMAP), &bitmap) == 211 | sizeof(BITMAP)) { 212 | auto bmWidthBytes = 213 | ((bitmap.bmWidth * bitmap.bmBitsPixel + 31) / 32) * 4; 214 | if (bmWidthBytes == bitmap.bmWidthBytes && 215 | (bitmap.bmBitsPixel == 32 || bitmap.bmBitsPixel == 24 || 216 | bitmap.bmBitsPixel == 16 || bitmap.bmBitsPixel == 8) && 217 | bitmap.bmWidth >= 4 && bitmap.bmWidth <= 64 && 218 | bitmap.bmHeight >= 4 && bitmap.bmHeight <= 64) { 219 | item.icon_bitmap = (size_t)result; 220 | if (config::current->context_menu 221 | .search_large_dwItemData_range) { 222 | dbgout("Found icon at offset: {}", offset); 223 | } 224 | break; 225 | } 226 | } 227 | } 228 | } 229 | } 230 | } 231 | 232 | if (info.fState & MFS_DISABLED) { 233 | item.disabled = true; 234 | } 235 | 236 | m.items.push_back(item); 237 | } 238 | 239 | m.parent_window = hWnd; 240 | m.is_top_level = is_top; 241 | return m; 242 | } 243 | } // namespace mb_shell 244 | -------------------------------------------------------------------------------- /src/shell/contextmenu/contextmenu.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "nanovg_wrapper.h" 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #define NOMINMAX 11 | #include 12 | 13 | namespace mb_shell { 14 | struct menu_item; 15 | struct menu_widget; 16 | struct menu { 17 | std::vector items; 18 | void *parent_window = nullptr; 19 | bool is_top_level = false; 20 | 21 | static menu construct_with_hmenu(HMENU hMenu, HWND hWnd, bool is_top = true); 22 | }; 23 | 24 | struct owner_draw_menu_info { 25 | HBITMAP bitmap; 26 | int width, height; 27 | }; 28 | 29 | struct menu_item { 30 | enum class type { 31 | button, 32 | spacer, 33 | } type = type::button; 34 | 35 | std::optional owner_draw{}; 36 | std::optional name; 37 | std::optional> action; 38 | std::optional)>> submenu; 39 | std::optional icon_bitmap; 40 | std::optional icon_svg; 41 | bool icon_updated = false; 42 | bool disabled = false; 43 | 44 | // the two below are only for information; set them changes nothing 45 | std::optional wID; 46 | std::optional name_resid; 47 | }; 48 | } // namespace mb_shell -------------------------------------------------------------------------------- /src/shell/contextmenu/menu_render.cc: -------------------------------------------------------------------------------- 1 | #include "menu_render.h" 2 | #include "Windows.h" 3 | #include "menu_widget.h" 4 | 5 | #include "../script/binding_types.h" 6 | #include "ui.h" 7 | #include 8 | #include 9 | 10 | #include "../logger.h" 11 | 12 | namespace mb_shell { 13 | std::optional menu_render::current{}; 14 | menu_render menu_render::create(int x, int y, menu menu) { 15 | if (auto res = ui::render_target::init_global(); !res) { 16 | MessageBoxW(NULL, L"Failed to initialize global render target", L"Error", 17 | MB_ICONERROR); 18 | return {nullptr, std::nullopt}; 19 | } 20 | 21 | constexpr int l_pad = 100, t_pad = -1; 22 | static auto rt = []() { 23 | auto rt = std::make_shared(); 24 | rt->transparent = true; 25 | rt->no_focus = true; 26 | rt->capture_all_input = true; 27 | rt->decorated = false; 28 | rt->topmost = true; 29 | rt->vsync = config::current->context_menu.vsync; 30 | 31 | if (config::current->avoid_resize_ui) { 32 | rt->width = 3840; 33 | rt->height = 2159; 34 | } 35 | 36 | if (auto res = rt->init(); !res) { 37 | MessageBoxW(NULL, L"Failed to initialize render target", L"Error", 38 | MB_ICONERROR); 39 | } 40 | 41 | nvgCreateFont(rt->nvg, "main", 42 | config::current->font_path_main.string().c_str()); 43 | nvgCreateFont(rt->nvg, "fallback", 44 | config::current->font_path_fallback.string().c_str()); 45 | nvgAddFallbackFont(rt->nvg, "main", "fallback"); 46 | return rt; 47 | }(); 48 | auto render = menu_render(rt, std::nullopt); 49 | auto current_js_context = std::make_shared( 50 | js::js_menu_context::$from_window(menu.parent_window)); 51 | 52 | rt->parent = menu.parent_window; 53 | 54 | // get the monitor in which the menu is being shown 55 | auto monitor = MonitorFromPoint({x, y}, MONITOR_DEFAULTTONEAREST); 56 | MONITORINFOEX monitor_info; 57 | monitor_info.cbSize = sizeof(MONITORINFOEX); 58 | GetMonitorInfo(monitor, &monitor_info); 59 | 60 | // set the position of the window to fullscreen in this monitor + padding 61 | 62 | dbgout("Monitor: {} {} {} {}", monitor_info.rcMonitor.left, 63 | monitor_info.rcMonitor.top, monitor_info.rcMonitor.right, 64 | monitor_info.rcMonitor.bottom); 65 | 66 | rt->set_position(monitor_info.rcMonitor.left, monitor_info.rcMonitor.top); 67 | if (!config::current->avoid_resize_ui) 68 | rt->resize( 69 | monitor_info.rcMonitor.right - monitor_info.rcMonitor.left + l_pad, 70 | monitor_info.rcMonitor.bottom - monitor_info.rcMonitor.top + t_pad); 71 | 72 | glfwMakeContextCurrent(rt->window); 73 | glfwSwapInterval(config::current->context_menu.vsync ? 1 : 0); 74 | 75 | rt->show(); 76 | auto menu_wid = std::make_shared( 77 | menu, 78 | // convert the x and y to the window coordinates 79 | x - monitor_info.rcMonitor.left, y - monitor_info.rcMonitor.top); 80 | rt->root->children.push_back(menu_wid); 81 | js::menu_info_basic_js menu_info{ 82 | .menu = std::make_shared(menu_wid->menu_wid), 83 | .context = current_js_context}; 84 | 85 | dbgout("[perf] JS plugins start"); 86 | auto before_js = rt->clock.now(); 87 | for (auto &listener : menu_callbacks_js) { 88 | listener->operator()(menu_info); 89 | } 90 | dbgout("[perf] JS plugins costed {}ms", 91 | std::chrono::duration_cast( 92 | rt->clock.now() - before_js) 93 | .count()); 94 | 95 | rt->on_focus_changed = [](bool focused) { 96 | if (!focused) { 97 | } 98 | }; 99 | 100 | dbgout("Current menu: {}", menu_render::current.has_value()); 101 | return render; 102 | } 103 | 104 | menu_render::menu_render(std::shared_ptr rt, 105 | std::optional selected_menu) 106 | : rt(std::move(rt)), selected_menu(selected_menu) { 107 | current = this; 108 | } 109 | menu_render::~menu_render() { 110 | if (this->rt) { 111 | current = nullptr; 112 | } 113 | } 114 | menu_render::menu_render(menu_render &&t) { 115 | current = this; 116 | 117 | rt = std::move(t.rt); 118 | selected_menu = std::move(t.selected_menu); 119 | } 120 | menu_render &menu_render::operator=(menu_render &&t) { 121 | current = this; 122 | rt = std::move(t.rt); 123 | selected_menu = std::move(t.selected_menu); 124 | return *this; 125 | } 126 | }; // namespace mb_shell -------------------------------------------------------------------------------- /src/shell/contextmenu/menu_render.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "../utils.h" 3 | #include "contextmenu.h" 4 | #include "ui.h" 5 | #include 6 | 7 | namespace mb_shell { 8 | struct menu_render { 9 | std::shared_ptr rt; 10 | std::optional selected_menu; 11 | bool light_color = is_light_mode(); 12 | static std::optional current; 13 | 14 | menu_render() = delete; 15 | menu_render(std::shared_ptr rt, 16 | std::optional selected_menu); 17 | 18 | ~menu_render(); 19 | const menu_render &operator=(const menu_render &) = delete; 20 | menu_render(menu_render &&t); 21 | menu_render &operator=(menu_render &&t); 22 | static menu_render create(int x, int y, menu menu); 23 | }; 24 | } // namespace mb_shell -------------------------------------------------------------------------------- /src/shell/contextmenu/menu_widget.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "../config.h" 3 | #include "animator.h" 4 | #include "contextmenu.h" 5 | #include "extra_widgets.h" 6 | #include "nanovg_wrapper.h" 7 | #include "ui.h" 8 | #include "widget.h" 9 | #include 10 | #include 11 | #include 12 | 13 | namespace mb_shell { 14 | 15 | struct menu_widget; 16 | 17 | struct menu_item_widget : public ui::widget { 18 | using super = ui::widget; 19 | menu_item item; 20 | ui::sp_anim_float opacity = anim_float(0, 200); 21 | menu_item_widget(); 22 | virtual void reset_appear_animation(float delay); 23 | }; 24 | 25 | struct menu_item_ownerdraw_widget : public menu_item_widget { 26 | using super = menu_item_widget; 27 | owner_draw_menu_info owner_draw; 28 | std::optional img{}; 29 | menu_item_ownerdraw_widget(menu_item item); 30 | void update(ui::update_context &ctx) override; 31 | void render(ui::nanovg_context ctx) override; 32 | void reset_appear_animation(float delay) override; 33 | }; 34 | 35 | struct menu_item_parent_widget : public menu_item_widget { 36 | using super = menu_item_widget; 37 | void update(ui::update_context &ctx) override; 38 | void reset_appear_animation(float delay) override; 39 | }; 40 | 41 | struct menu_item_normal_widget : public menu_item_widget { 42 | using super = menu_item_widget; 43 | ui::sp_anim_float opacity = anim_float(0, 200); 44 | float text_padding = config::current->context_menu.theme.text_padding; 45 | float margin = config::current->context_menu.theme.margin; 46 | bool has_icon_padding = false; 47 | float padding = config::current->context_menu.theme.padding; 48 | float icon_padding = config::current->context_menu.theme.icon_padding; 49 | float right_icon_padding = 50 | config::current->context_menu.theme.right_icon_padding; 51 | menu_item_normal_widget(menu_item item); 52 | void reset_appear_animation(float delay) override; 53 | 54 | std::optional icon_img{}; 55 | std::optional icon_unfold_img{}; 56 | 57 | std::shared_ptr submenu_wid = nullptr; 58 | float show_submenu_timer = 0.f; 59 | 60 | ui::sp_anim_float bg_opacity = anim_float(0, 200); 61 | void render(ui::nanovg_context ctx) override; 62 | void update(ui::update_context &ctx) override; 63 | float measure_width(ui::update_context &ctx) override; 64 | bool check_hit(const ui::update_context &ctx) override; 65 | 66 | void hide_submenu(); 67 | void show_submenu(ui::update_context &ctx); 68 | void reload_icon_img(ui::nanovg_context ctx); 69 | }; 70 | 71 | enum class popup_direction { 72 | // 第一象限 ~ 第四象限 73 | top_left, 74 | top_right, 75 | bottom_left, 76 | bottom_right, 77 | }; 78 | 79 | struct menu_widget : public ui::widget_flex { 80 | using super = ui::widget_flex; 81 | float bg_padding_vertical = 6; 82 | 83 | float max_height = 99999; 84 | float actual_height = 0; 85 | ui::sp_anim_float scroll_top = 86 | anim_float(0, 200, ui::easing_type::ease_in_out); 87 | std::shared_ptr bg; 88 | 89 | std::shared_ptr bg_submenu; 90 | std::shared_ptr current_submenu; 91 | std::vector> rendering_submenus; 92 | std::vector> item_widgets; 93 | menu_widget *parent_menu = nullptr; 94 | 95 | std::shared_ptr create_bg(bool is_main); 96 | menu menu_data; 97 | menu_widget(); 98 | popup_direction direction = popup_direction::bottom_right; 99 | void init_from_data(menu menu_data); 100 | bool animate_appear_started = false; 101 | void reset_animation(bool reverse = false); 102 | void update(ui::update_context &ctx) override; 103 | 104 | void update_icon_width(); 105 | 106 | void render(ui::nanovg_context ctx) override; 107 | 108 | bool check_hit(const ui::update_context &ctx) override; 109 | void close(); 110 | }; 111 | 112 | struct mouse_menu_widget_main : public ui::widget { 113 | float anchor_x = 0, anchor_y = 0; 114 | mouse_menu_widget_main(menu menu_data, float x, float y); 115 | bool position_calibrated = false, direction_calibrated = false; 116 | popup_direction direction; 117 | std::shared_ptr menu_wid; 118 | 119 | void update(ui::update_context &ctx); 120 | 121 | void render(ui::nanovg_context ctx); 122 | 123 | static std::pair 124 | calculate_position(menu_widget *menu_wid, ui::update_context &ctx, 125 | float anchor_x, float anchor_y, popup_direction direction); 126 | 127 | static popup_direction calculate_direction( 128 | menu_widget *menu_wid, ui::update_context &ctx, float anchor_x, 129 | float anchor_y, 130 | popup_direction prefer_direction = popup_direction::bottom_right); 131 | 132 | void calibrate_position(ui::update_context &ctx, bool animated = true); 133 | void calibrate_direction(ui::update_context &ctx); 134 | }; 135 | 136 | } // namespace mb_shell -------------------------------------------------------------------------------- /src/shell/entry.cc: -------------------------------------------------------------------------------- 1 | 2 | #include "GLFW/glfw3.h" 3 | #include "blook/blook.h" 4 | 5 | #include "config.h" 6 | #include "entry.h" 7 | #include "error_handler.h" 8 | #include "res_string_loader.h" 9 | #include "script/binding_types.h" 10 | #include "script/quickjspp.hpp" 11 | #include "script/script.h" 12 | #include "ui.h" 13 | #include "utils.h" 14 | 15 | #include "./contextmenu/contextmenu.h" 16 | #include "./contextmenu/menu_render.h" 17 | #include "./contextmenu/menu_widget.h" 18 | 19 | #include "fix_win11_menu.h" 20 | 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | #include 36 | #include 37 | 38 | #include 39 | #include 40 | #include 41 | #include 42 | #include 43 | 44 | #define NOMINMAX 45 | #include 46 | 47 | namespace mb_shell { 48 | window_proc_hook entry::main_window_loop_hook{}; 49 | void main() { 50 | set_thread_locale_utf8(); 51 | 52 | AllocConsole(); 53 | freopen("CONOUT$", "w", stdout); 54 | freopen("CONOUT$", "w", stderr); 55 | freopen("CONIN$", "r", stdin); 56 | ShowWindow(GetConsoleWindow(), SW_HIDE); 57 | 58 | install_error_handlers(); 59 | config::run_config_loader(); 60 | 61 | res_string_loader::init(); 62 | fix_win11_menu::install(); 63 | 64 | static std::atomic_bool has_active_menu = false; 65 | std::thread([]() { 66 | script_context ctx; 67 | 68 | auto data_dir = config::data_directory(); 69 | auto script_dir = data_dir / "scripts"; 70 | 71 | if (!std::filesystem::exists(script_dir)) 72 | std::filesystem::create_directories(script_dir); 73 | 74 | ctx.watch_folder(script_dir, [&]() { return !has_active_menu.load(); }); 75 | }).detach(); 76 | 77 | auto proc = blook::Process::self(); 78 | auto win32u = proc->module("win32u.dll"); 79 | auto user32 = proc->module("user32.dll"); 80 | 81 | auto NtUserTrackPopupMenu = win32u.value()->exports("NtUserTrackPopupMenuEx"); 82 | static auto NtUserTrackHook = NtUserTrackPopupMenu->inline_hook(); 83 | 84 | std::set_terminate([]() { 85 | auto eptr = std::current_exception(); 86 | if (eptr) { 87 | try { 88 | std::rethrow_exception(eptr); 89 | } catch (const std::exception &e) { 90 | std::cerr << "Uncaught exception: " << e.what() << std::endl; 91 | } catch (...) { 92 | std::cerr << "Uncaught exception of unknown type" << std::endl; 93 | } 94 | 95 | ShowWindow(GetConsoleWindow(), SW_SHOW); 96 | std::getchar(); 97 | } 98 | std::abort(); 99 | }); 100 | 101 | std::thread([]() { 102 | if (auto res = ui::render_target::init_global(); !res) { 103 | MessageBoxW(NULL, L"Failed to initialize global render target", L"Error", 104 | MB_ICONERROR); 105 | return; 106 | } 107 | }).detach(); 108 | 109 | NtUserTrackHook->install(+[](HMENU hMenu, int64_t uFlags, int64_t x, 110 | int64_t y, HWND hWnd, int64_t lptpm) { 111 | if (GetPropW(hWnd, L"COwnerDrawPopupMenu_This") && 112 | config::current->context_menu.ignore_owner_draw) { 113 | return NtUserTrackHook->call_trampoline(hMenu, uFlags, x, y, 114 | hWnd, lptpm); 115 | } 116 | 117 | entry::main_window_loop_hook.install(hWnd); 118 | 119 | has_active_menu = true; 120 | 121 | perf_counter perf("TrackPopupMenuEx"); 122 | menu menu = menu::construct_with_hmenu(hMenu, hWnd); 123 | perf.end("construct_with_hmenu"); 124 | auto menu_render = menu_render::create(x, y, menu); 125 | menu_render.rt->last_time = menu_render.rt->clock.now(); 126 | perf.end("menu_render::create"); 127 | menu_render.rt->start_loop(); 128 | 129 | has_active_menu = false; 130 | 131 | if (menu_render.selected_menu && !(uFlags & TPM_NONOTIFY)) { 132 | PostMessageW(hWnd, WM_COMMAND, *menu_render.selected_menu, 0); 133 | PostMessageW(hWnd, WM_NULL, 0, 0); 134 | } 135 | 136 | return (int32_t)menu_render.selected_menu.value_or(0); 137 | }); 138 | } 139 | } // namespace mb_shell 140 | 141 | int APIENTRY DllMain(HINSTANCE hInstance, DWORD fdwReason, LPVOID lpvReserved) { 142 | switch (fdwReason) { 143 | case DLL_PROCESS_ATTACH: { 144 | auto cmdline = std::string(GetCommandLineA()); 145 | 146 | std::ranges::transform(cmdline, cmdline.begin(), tolower); 147 | 148 | mb_shell::main(); 149 | break; 150 | } 151 | } 152 | return 1; 153 | } -------------------------------------------------------------------------------- /src/shell/entry.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "window_proc_hook.h" 4 | 5 | namespace mb_shell { 6 | struct entry { 7 | static window_proc_hook main_window_loop_hook; 8 | }; 9 | } // namespace mb_shell -------------------------------------------------------------------------------- /src/shell/error_handler.cc: -------------------------------------------------------------------------------- 1 | #include "error_handler.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include "build_info.h" 12 | #include "config.h" 13 | 14 | #include "utils.h" 15 | 16 | #include "Windows.h" 17 | #include 18 | 19 | void show_console() { 20 | if (!GetConsoleWindow()) { 21 | AllocConsole(); 22 | freopen("CONOUT$", "w", stdout); 23 | freopen("CONOUT$", "w", stderr); 24 | } 25 | ShowWindow(GetConsoleWindow(), SW_SHOW); 26 | SetForegroundWindow(GetConsoleWindow()); 27 | } 28 | 29 | inline void output_crash_header(std::stringstream &ss) { 30 | ss << "Breeze Shell " << BREEZE_VERSION << " crash report" << std::endl; 31 | ss << "----------------------------------------" << std::endl; 32 | ss << "Build date: " << BREEZE_BUILD_DATE_TIME << std::endl; 33 | ss << "Git commit hash: " << BREEZE_GIT_COMMIT_HASH << std::endl; 34 | ss << "Commit page: https://github.com/std-microblock/breeze-shell/commit/" 35 | << BREEZE_GIT_COMMIT_HASH << std::endl; 36 | ss << "Git branch: " << BREEZE_GIT_BRANCH_NAME << std::endl; 37 | ss << "Data directory: " << mb_shell::config::data_directory() << std::endl; 38 | ss << "Windows version: " 39 | << (mb_shell::is_win11_or_later() ? "11 or later" : "10 or earlier") 40 | << std::endl; 41 | ss << "----------------------------------------" << std::endl; 42 | ss << "Config:" << std::endl; 43 | ss << mb_shell::config::dump_config() << std::endl; 44 | ss << "----------------------------------------" << std::endl; 45 | } 46 | 47 | std::wstring GetBacktrace(CONTEXT *contextRecord) { 48 | std::wstring info; 49 | wchar_t symbol_mem[sizeof(IMAGEHLP_SYMBOL64) + 256]; 50 | auto symbol = (IMAGEHLP_SYMBOL64 *)symbol_mem; 51 | 52 | std::string current_module_path = std::string(MAX_PATH, '\0'); 53 | GetModuleFileNameA(nullptr, current_module_path.data(), 54 | current_module_path.size()); 55 | 56 | std::filesystem::path current_module_path_fs = current_module_path; 57 | auto current_module_folder = current_module_path_fs.parent_path(); 58 | 59 | // Initialize the symbol handler 60 | SymInitializeW(GetCurrentProcess(), 61 | current_module_folder.c_str(), TRUE); 62 | 63 | // Initialize the stack frame 64 | STACKFRAME64 stackFrame = {0}; 65 | 66 | #ifdef _WIN64 67 | stackFrame.AddrPC.Offset = contextRecord->Rip; 68 | stackFrame.AddrPC.Mode = AddrModeFlat; 69 | stackFrame.AddrFrame.Offset = contextRecord->Rbp; 70 | stackFrame.AddrFrame.Mode = AddrModeFlat; 71 | stackFrame.AddrStack.Offset = contextRecord->Rsp; 72 | stackFrame.AddrStack.Mode = AddrModeFlat; 73 | #else 74 | stackFrame.AddrPC.Offset = contextRecord->Eip; 75 | stackFrame.AddrPC.Mode = AddrModeFlat; 76 | stackFrame.AddrFrame.Offset = contextRecord->Ebp; 77 | stackFrame.AddrFrame.Mode = AddrModeFlat; 78 | stackFrame.AddrStack.Offset = contextRecord->Esp; 79 | stackFrame.AddrStack.Mode = AddrModeFlat; 80 | #endif 81 | 82 | DWORD64 displacement = 0; 83 | // Walk the call stack and append each frame to the string 84 | while (StackWalk64( 85 | #ifdef _WIN64 86 | IMAGE_FILE_MACHINE_AMD64 87 | #else 88 | IMAGE_FILE_MACHINE_I386 89 | #endif 90 | 91 | , 92 | GetCurrentProcess(), GetCurrentThread(), &stackFrame, contextRecord, 93 | nullptr, SymFunctionTableAccess64, SymGetModuleBase64, nullptr)) { 94 | // Get the module name and offset for this frame 95 | DWORD64 moduleBase = 96 | SymGetModuleBase64(GetCurrentProcess(), stackFrame.AddrPC.Offset); 97 | wchar_t moduleName[MAX_PATH]; 98 | GetModuleFileNameW((HMODULE)moduleBase, moduleName, MAX_PATH); 99 | DWORD64 offset = stackFrame.AddrPC.Offset - moduleBase; 100 | 101 | symbol->SizeOfStruct = sizeof(IMAGEHLP_SYMBOL64); 102 | symbol->MaxNameLength = 255; 103 | 104 | wchar_t name[256] = {}; 105 | SymGetSymFromAddr64(GetCurrentProcess(), stackFrame.AddrPC.Offset, 106 | &displacement, symbol); 107 | 108 | DWORD dwDisplacement; 109 | IMAGEHLP_LINE64 line; 110 | 111 | SymGetLineFromAddr64(GetCurrentProcess(), stackFrame.AddrPC.Offset, 112 | &dwDisplacement, &line); 113 | 114 | UnDecorateSymbolNameW(mb_shell::utf8_to_wstring(symbol->Name).c_str(), name, 115 | 256, UNDNAME_COMPLETE); 116 | 117 | // Append the frame to the string 118 | wchar_t frameInfo[1024]; 119 | swprintf_s(frameInfo, L"%s + %I64X", moduleName, offset); 120 | info += std::wstring(frameInfo) + std::wstring(L"(") + std::wstring(name) + 121 | std::wstring(L") at line ") + std::to_wstring(line.LineNumber) + L"\n"; 122 | } 123 | 124 | // Cleanup the symbol handler 125 | SymCleanup(GetCurrentProcess()); 126 | 127 | // Return the backtrace string 128 | return info; 129 | } 130 | 131 | std::string GetExceptionName(EXCEPTION_POINTERS *ExceptionInfo) { 132 | std::string exceptionName; 133 | switch (ExceptionInfo->ExceptionRecord->ExceptionCode) { 134 | case EXCEPTION_ACCESS_VIOLATION: 135 | exceptionName = "ACCESS_VIOLATION"; 136 | break; 137 | case EXCEPTION_ARRAY_BOUNDS_EXCEEDED: 138 | exceptionName = "ARRAY_BOUNDS_EXCEEDED"; 139 | break; 140 | case EXCEPTION_BREAKPOINT: 141 | exceptionName = "BREAKPOINT"; 142 | break; 143 | case EXCEPTION_DATATYPE_MISALIGNMENT: 144 | exceptionName = "DATATYPE_MISALIGNMENT"; 145 | break; 146 | case EXCEPTION_FLT_DENORMAL_OPERAND: 147 | exceptionName = "FLT_DENORMAL_OPERAND"; 148 | break; 149 | case EXCEPTION_FLT_DIVIDE_BY_ZERO: 150 | exceptionName = "FLT_DIVIDE_BY_ZERO"; 151 | break; 152 | case EXCEPTION_FLT_INEXACT_RESULT: 153 | exceptionName = "FLT_INEXACT_RESULT"; 154 | break; 155 | case EXCEPTION_FLT_INVALID_OPERATION: 156 | exceptionName = "FLT_INVALID_OPERATION"; 157 | break; 158 | case EXCEPTION_FLT_OVERFLOW: 159 | exceptionName = "FLT_OVERFLOW"; 160 | break; 161 | case EXCEPTION_FLT_STACK_CHECK: 162 | exceptionName = "FLT_STACK_CHECK"; 163 | break; 164 | case EXCEPTION_FLT_UNDERFLOW: 165 | exceptionName = "FLT_UNDERFLOW"; 166 | break; 167 | case EXCEPTION_ILLEGAL_INSTRUCTION: 168 | exceptionName = "ILLEGAL_INSTRUCTION"; 169 | break; 170 | case EXCEPTION_IN_PAGE_ERROR: 171 | exceptionName = "IN_PAGE_ERROR"; 172 | break; 173 | case EXCEPTION_INT_DIVIDE_BY_ZERO: 174 | exceptionName = "INT_DIVIDE_BY_ZERO"; 175 | break; 176 | case EXCEPTION_INT_OVERFLOW: 177 | exceptionName = "INT_OVERFLOW"; 178 | break; 179 | case EXCEPTION_INVALID_DISPOSITION: 180 | exceptionName = "INVALID_DISPOSITION"; 181 | break; 182 | case EXCEPTION_NONCONTINUABLE_EXCEPTION: 183 | exceptionName = "NONCONTINUABLE_EXCEPTION"; 184 | break; 185 | case EXCEPTION_PRIV_INSTRUCTION: 186 | exceptionName = "PRIV_INSTRUCTION"; 187 | break; 188 | case EXCEPTION_SINGLE_STEP: 189 | exceptionName = "SINGLE_STEP"; 190 | break; 191 | case EXCEPTION_STACK_OVERFLOW: 192 | exceptionName = "STACK_OVERFLOW"; 193 | break; 194 | default: 195 | exceptionName = "UNKNOWN"; 196 | break; 197 | } 198 | return exceptionName; 199 | } 200 | 201 | void mb_shell::install_error_handlers() { 202 | SetUnhandledExceptionFilter([](PEXCEPTION_POINTERS ex) -> LONG { 203 | show_console(); 204 | 205 | std::ofstream file(config::data_directory().string() + 206 | "\\crash_report.txt"); 207 | 208 | std::stringstream ss; 209 | output_crash_header(ss); 210 | ss << "Exception code: " << std::hex << ex->ExceptionRecord->ExceptionCode 211 | << "(" << GetExceptionName(ex) << ")" << std::endl; 212 | ss << "Exception flags: " << std::hex << ex->ExceptionRecord->ExceptionFlags 213 | << std::endl; 214 | ss << "Exception address: " << std::hex 215 | << ex->ExceptionRecord->ExceptionAddress << std::endl; 216 | ss << "Registers:" << std::endl; 217 | 218 | ss << "RAX: " << std::hex << ex->ContextRecord->Rax << std::endl; 219 | ss << "RBX: " << std::hex << ex->ContextRecord->Rbx << std::endl; 220 | ss << "RCX: " << std::hex << ex->ContextRecord->Rcx << std::endl; 221 | ss << "RDX: " << std::hex << ex->ContextRecord->Rdx << std::endl; 222 | ss << "R8: " << std::hex << ex->ContextRecord->R8 << std::endl; 223 | ss << "R9: " << std::hex << ex->ContextRecord->R9 << std::endl; 224 | ss << "R10: " << std::hex << ex->ContextRecord->R10 << std::endl; 225 | ss << "R11: " << std::hex << ex->ContextRecord->R11 << std::endl; 226 | ss << "R12: " << std::hex << ex->ContextRecord->R12 << std::endl; 227 | ss << "R13: " << std::hex << ex->ContextRecord->R13 << std::endl; 228 | ss << "R14: " << std::hex << ex->ContextRecord->R14 << std::endl; 229 | ss << "R15: " << std::hex << ex->ContextRecord->R15 << std::endl; 230 | ss << "RDI: " << std::hex << ex->ContextRecord->Rdi << std::endl; 231 | ss << "RSI: " << std::hex << ex->ContextRecord->Rsi << std::endl; 232 | ss << "RBP: " << std::hex << ex->ContextRecord->Rbp << std::endl; 233 | ss << "RSP: " << std::hex << ex->ContextRecord->Rsp << std::endl; 234 | ss << "RIP: " << std::hex << ex->ContextRecord->Rip << std::endl; 235 | 236 | ss << "Stack trace:" << std::endl; 237 | ss << wstring_to_utf8(GetBacktrace(ex->ContextRecord)) << std::endl; 238 | 239 | if (file.is_open()) { 240 | file << ss.str(); 241 | file.flush(); 242 | file.close(); 243 | } 244 | std::cerr << ss.str(); 245 | 246 | Sleep(5000); 247 | return EXCEPTION_CONTINUE_EXECUTION; 248 | }); 249 | 250 | std::set_terminate([]() { 251 | show_console(); 252 | 253 | std::stringstream ss; 254 | output_crash_header(ss); 255 | try { 256 | throw std::current_exception(); 257 | } catch (const std::exception &e) { 258 | ss << "Uncaught exception: " << e.what() << std::endl; 259 | } catch (...) { 260 | ss << "Uncaught exception of unknown type" << std::endl; 261 | } 262 | 263 | CONTEXT ctx; 264 | RtlCaptureContext(&ctx); 265 | ss << "Stack trace:" << std::endl; 266 | ss << wstring_to_utf8(GetBacktrace(&ctx)) << std::endl; 267 | 268 | std::ofstream file(config::data_directory().string() + 269 | "\\crash_report.txt"); 270 | 271 | if (file.is_open()) { 272 | file << ss.str(); 273 | file.flush(); 274 | file.close(); 275 | } 276 | 277 | std::cerr << ss.str(); 278 | 279 | Sleep(5000); 280 | }); 281 | } 282 | -------------------------------------------------------------------------------- /src/shell/error_handler.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | namespace mb_shell { 3 | void install_error_handlers(); 4 | } -------------------------------------------------------------------------------- /src/shell/fix_win11_menu.cc: -------------------------------------------------------------------------------- 1 | #include "fix_win11_menu.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "blook/blook.h" 10 | #include "blook/memo.h" 11 | #include "utils.h" 12 | #include "zasm/base/immediate.hpp" 13 | #include "zasm/x86/mnemonic.hpp" 14 | #include "zasm/x86/register.hpp" 15 | 16 | // https://stackoverflow.com/questions/937044/determine-path-to-registry-key-from-hkey-handle-in-c 17 | #include 18 | #define WIN32_NO_STATUS 19 | #include 20 | #include 21 | #pragma comment(lib, "ntdll") 22 | #include 23 | #include 24 | 25 | #define REG_KEY_PATH_LENGTH 1024 26 | 27 | typedef enum _KEY_INFORMATION_CLASS { 28 | KeyBasicInformation, 29 | KeyNodeInformation, 30 | KeyFullInformation, 31 | KeyNameInformation, 32 | KeyCachedInformation, 33 | KeyFlagsInformation, 34 | KeyVirtualizationInformation, 35 | KeyHandleTagsInformation, 36 | KeyTrustInformation, 37 | KeyLayerInformation, 38 | MaxKeyInfoClass 39 | } KEY_INFORMATION_CLASS; 40 | 41 | typedef struct _KEY_NAME_INFORMATION { 42 | ULONG NameLength; 43 | WCHAR Name[1]; 44 | } KEY_NAME_INFORMATION, *PKEY_NAME_INFORMATION; 45 | 46 | EXTERN_C NTSYSAPI NTSTATUS NTAPI NtQueryKey( 47 | __in HANDLE /* KeyHandle */, 48 | __in KEY_INFORMATION_CLASS /* KeyInformationClass */, 49 | __out_opt PVOID /* KeyInformation */, __in ULONG /* Length */, 50 | __out ULONG * /* ResultLength */ 51 | ); 52 | 53 | std::wstring RegQueryKeyPath(HKEY hKey) { 54 | std::wstring keyPath; 55 | 56 | NTSTATUS Status; 57 | 58 | std::vector Buffer(FIELD_OFFSET(KEY_NAME_INFORMATION, Name) + 59 | sizeof(WCHAR) * REG_KEY_PATH_LENGTH); 60 | KEY_NAME_INFORMATION *pkni; 61 | ULONG Length; 62 | 63 | TryAgain: 64 | Status = NtQueryKey(hKey, KeyNameInformation, Buffer.data(), Buffer.size(), 65 | &Length); 66 | switch (Status) { 67 | case STATUS_BUFFER_TOO_SMALL: 68 | case STATUS_BUFFER_OVERFLOW: 69 | Buffer.resize(Length); 70 | goto TryAgain; 71 | case STATUS_SUCCESS: 72 | pkni = reinterpret_cast(Buffer.data()); 73 | keyPath.assign(pkni->Name, pkni->NameLength / sizeof(WCHAR)); 74 | default: 75 | break; 76 | } 77 | 78 | return keyPath; 79 | } 80 | void mb_shell::fix_win11_menu::install() { 81 | auto proc = blook::Process::self(); 82 | 83 | // approch 1: simulated reg edit 84 | auto advapi32 = proc->module("kernelbase.dll"); 85 | 86 | auto RegGetValueW = advapi32.value()->exports("RegGetValueW"); 87 | static auto RegGetValueHook = RegGetValueW->inline_hook(); 88 | 89 | RegGetValueHook->install( 90 | (void *)+[](HKEY hkey, LPCWSTR lpSubKey, LPCWSTR lpValue, DWORD dwFlags, 91 | LPDWORD pdwType, PVOID pvData, LPDWORD pcbData) { 92 | // simulate 93 | // reg.exe add 94 | // 95 | // 96 | // "HKCU\Software\Classes\CLSID\{86ca1aa0-34aa-4e8b-a509-50c905bae2a2}\InprocServer32" 97 | // /f /ve 98 | auto path = wstring_to_utf8(RegQueryKeyPath(hkey)); 99 | if (path.ends_with("\\CLSID\\{86ca1aa0-34aa-4e8b-a509-50c905bae2a2}" 100 | "\\InprocServer32")) { 101 | if (pvData != nullptr && pcbData != nullptr) { 102 | *pcbData = 0; 103 | } 104 | if (pdwType != nullptr) { 105 | *pdwType = REG_SZ; 106 | } 107 | return ERROR_SUCCESS; 108 | } else 109 | return RegGetValueHook->call_trampoline( 110 | hkey, lpSubKey, lpValue, dwFlags, pdwType, pvData, pcbData); 111 | }); 112 | 113 | // approch 2: patch the shell32.dll to predent the shift key is pressed 114 | std::thread([=]() { 115 | if (auto shell32 = proc->module("shell32.dll")) { 116 | // mov ecx, 10 117 | // call GetKeyState/GetAsyncKeyState 118 | auto disasm = shell32.value()->section(".text")->disassembly(); 119 | 120 | auto patch_area = [&](auto mem) { 121 | for (auto it = mem.begin(); it != mem.end(); ++it) { 122 | auto &insn = *it; 123 | if (insn->getMnemonic() == zasm::x86::Mnemonic::Mov) { 124 | if (insn->getOperand(0) == zasm::x86::ecx && 125 | insn->getOperand(1).template holds() && 126 | insn->getOperand(1) 127 | .template get() 128 | .template value() == 0x10) { 129 | auto &next = *std::next(it); 130 | if (next->getMnemonic() == zasm::x86::Mnemonic::Call && 131 | next->getOperand(0).template holds()) { 132 | insn.ptr() 133 | .reassembly([](auto a) { 134 | a.mov(zasm::x86::eax, 0x0); 135 | a.bts(zasm::x86::edi, 0x8); 136 | a.nop(); 137 | a.nop(); 138 | a.nop(); 139 | }) 140 | .patch(); 141 | 142 | return true; 143 | } 144 | } 145 | } 146 | } 147 | 148 | return false; 149 | }; 150 | 151 | // the function to determine if show win10 menu or win11 menu calls 152 | // SetMessageExtraInfo, so we use it as a hint 153 | auto extraInfo = 154 | GetProcAddress(LoadLibraryA("user32.dll"), "SetMessageExtraInfo"); 155 | for (auto &ins : disasm) { 156 | if (ins->getMnemonic() == zasm::x86::Mnemonic::Call) { 157 | auto xrefs = ins.xrefs(); 158 | if (xrefs.empty()) 159 | continue; 160 | if (auto ptr = xrefs[0].try_read(); 161 | ptr.has_value() && ptr.value() == extraInfo) { 162 | if (patch_area(ins.ptr() 163 | .find_upwards({0x40, 0x55}) 164 | ->range_size(0x150) 165 | .disassembly())) 166 | break; 167 | } 168 | } 169 | } 170 | } 171 | }).detach(); 172 | } 173 | -------------------------------------------------------------------------------- /src/shell/fix_win11_menu.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | namespace mb_shell { 3 | struct fix_win11_menu { 4 | static void install(); 5 | }; 6 | } -------------------------------------------------------------------------------- /src/shell/logger.cc: -------------------------------------------------------------------------------- 1 | #include "logger.h" 2 | #include "config.h" 3 | #include "utils.h" 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | 11 | namespace mb_shell { 12 | void append_debug_string(const std::string &str) { 13 | static std::mutex mutex; 14 | static std::ofstream file(config::data_directory() / "debug.log", 15 | std::ios::app); 16 | 17 | std::lock_guard lock(mutex); 18 | 19 | file << str; 20 | file.flush(); 21 | 22 | printf("%s", str.c_str()); 23 | OutputDebugStringA(str.c_str()); 24 | } 25 | } // namespace mb_shell -------------------------------------------------------------------------------- /src/shell/logger.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | 6 | namespace mb_shell { 7 | void append_debug_string(const std::string &str); 8 | template 9 | void dbgout(const std::format_string fmt, types&&... args) { 10 | std::string str = std::format(fmt, std::forward(args)...); 11 | append_debug_string(str + "\n"); 12 | } 13 | } -------------------------------------------------------------------------------- /src/shell/res_string_loader.cc: -------------------------------------------------------------------------------- 1 | #include "res_string_loader.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "config.h" 11 | #include "utils.h" 12 | 13 | #include "blook/blook.h" 14 | 15 | #include "logger.h" 16 | 17 | #include "Windows.h" 18 | 19 | namespace mb_shell { 20 | static std::mutex lock_str_data; 21 | static std::unordered_map 23 | str_data; 24 | static std::unordered_map module_name_cache; 25 | 26 | res_string_loader::string_id res_string_loader::string_to_id(std::wstring str) { 27 | std::lock_guard lock(lock_str_data); 28 | auto it = str_data.find(str); 29 | if (it != str_data.end()) { 30 | return it->second; 31 | } 32 | return std::hash{}(str); 33 | } 34 | 35 | std::string get_module_name_from_instance(HINSTANCE hInstance) { 36 | char buffer[MAX_PATH]; 37 | GetModuleFileNameA(hInstance, buffer, MAX_PATH); 38 | return std::filesystem::path(buffer).filename().string(); 39 | } 40 | 41 | size_t store_module_name(HINSTANCE hInstance) { 42 | std::string module_name = get_module_name_from_instance(hInstance); 43 | auto mod_hash = std::hash{}(module_name); 44 | module_name_cache[mod_hash] = module_name; 45 | return mod_hash; 46 | } 47 | 48 | void res_string_loader::init_hook() { 49 | static std::atomic_bool inited = false; 50 | if (inited.exchange(true)) { 51 | return; 52 | } 53 | 54 | auto proc = blook::Process::self(); 55 | auto kernelbase = proc->module("kernelbase.dll").value(); 56 | 57 | static auto LoadStringWHook = 58 | kernelbase->exports("LoadStringW")->inline_hook(); 59 | LoadStringWHook->install(+[](HINSTANCE hInstance, UINT uID, LPWSTR lpBuffer, 60 | int cchBufferMax) -> int { 61 | auto res = LoadStringWHook->call_trampoline(hInstance, uID, lpBuffer, 62 | cchBufferMax); 63 | if (res > 0) { 64 | std::wstring str(lpBuffer, res); 65 | std::lock_guard lock(lock_str_data); 66 | if (str_data.find(str) != str_data.end()) 67 | return res; 68 | str_data[str] = {uID, store_module_name(hInstance)}; 69 | } 70 | return res; 71 | }); 72 | 73 | static auto LoadStringAHook = 74 | kernelbase->exports("LoadStringA")->inline_hook(); 75 | 76 | LoadStringAHook->install(+[](HINSTANCE hInstance, UINT uID, LPSTR lpBuffer, 77 | int cchBufferMax) -> int { 78 | auto res = LoadStringAHook->call_trampoline(hInstance, uID, lpBuffer, 79 | cchBufferMax); 80 | if (res > 0) { 81 | std::string str(lpBuffer, res); 82 | std::lock_guard lock(lock_str_data); 83 | auto s = utf8_to_wstring(str); 84 | if (str_data.find(s) != str_data.end()) 85 | return res; 86 | str_data[s] = {uID, store_module_name(hInstance)}; 87 | } 88 | 89 | return res; 90 | }); 91 | } 92 | std::string res_string_loader::string_to_id_string(std::wstring str) { 93 | auto id = string_to_id(str); 94 | if (auto *p = std::get_if(&id)) { 95 | return std::to_string(p->id) + "@" + module_name_cache[p->module]; 96 | } else { 97 | return std::to_string(std::get(id)) + "@0"; 98 | } 99 | } 100 | void res_string_loader::init() { 101 | std::thread([]() { 102 | init_known_strings(); 103 | if (config::current->res_string_loader_use_hook) 104 | init_hook(); 105 | }).detach(); 106 | } 107 | 108 | void EnumerateStringResources( 109 | HMODULE mod, std::function callback) { 110 | EnumResourceNamesW( 111 | mod, MAKEINTRESOURCEW(6), 112 | +[](HMODULE hModule, LPCWSTR /*lpType*/, LPWSTR lpName, 113 | LONG_PTR lParam) -> BOOL { 114 | auto &cb = 115 | *reinterpret_cast *>( 116 | lParam); 117 | 118 | if (!IS_INTRESOURCE(lpName)) 119 | return TRUE; 120 | 121 | HRSRC hRes = FindResourceW(hModule, lpName, MAKEINTRESOURCEW(6)); 122 | if (!hRes) 123 | return TRUE; 124 | 125 | HGLOBAL hData = LoadResource(hModule, hRes); 126 | if (!hData) 127 | return TRUE; 128 | 129 | const BYTE *pData = static_cast(LockResource(hData)); 130 | if (!pData) 131 | return TRUE; 132 | 133 | DWORD dwSize = SizeofResource(hModule, hRes); 134 | DWORD pos = 0; 135 | 136 | for (int i = 0; i < 16 && pos < dwSize; ++i) { 137 | WORD len = *reinterpret_cast(pData + pos); 138 | pos += sizeof(WORD); 139 | 140 | if (len == 0) 141 | continue; 142 | 143 | if (pos + len * sizeof(WCHAR) > dwSize) 144 | break; 145 | 146 | std::wstring_view strView( 147 | reinterpret_cast(pData + pos), len); 148 | cb(strView, 149 | static_cast(reinterpret_cast(lpName)) * 16 + i); 150 | 151 | pos += len * sizeof(WCHAR); 152 | } 153 | 154 | return TRUE; 155 | }, 156 | reinterpret_cast(&callback)); 157 | } 158 | 159 | void load_all_res_strings(std::string module) { 160 | HINSTANCE hInstance = GetModuleHandleA(module.data()); 161 | static std::unordered_set loaded_modules; 162 | if (loaded_modules.contains(hInstance)) { 163 | return; 164 | } 165 | if (!hInstance) { 166 | return; 167 | } 168 | auto mod_hash = store_module_name(hInstance); 169 | 170 | EnumerateStringResources(hInstance, [&](std::wstring_view str, size_t id) { 171 | std::lock_guard lock(lock_str_data); 172 | if (str.length() > 60) 173 | return; 174 | auto s = std::wstring(str); 175 | if (str_data.find(s) != str_data.end()) 176 | return; 177 | str_data[s] = {id, mod_hash}; 178 | }); 179 | } 180 | 181 | void res_string_loader::init_known_strings() { 182 | auto res_dlls = { 183 | "shell32.dll", "acppage.dll", "ntshrui.dll", 184 | "appresolver.dll", "windows.storage.dll", "explorerframe.dll", 185 | "explorer.exe", "user32.dll", "wpdshext.dll", 186 | "display.dll", "themecpl.dll", "regedit.exe", 187 | "powershell.exe", "stobject.dll", "fvecpl.dll", 188 | "twext.dll", "twinui.dll", "twinui.pcshell.dll", 189 | "isoburn.exe"}; 190 | 191 | auto now = std::chrono::high_resolution_clock::now(); 192 | for (auto &dll : res_dlls) { 193 | load_all_res_strings(dll); 194 | } 195 | dbgout("[perf] init_known_strings took {}ms", 196 | std::chrono::duration_cast( 197 | std::chrono::high_resolution_clock::now() - now) 198 | .count()); 199 | } 200 | } // namespace mb_shell -------------------------------------------------------------------------------- /src/shell/res_string_loader.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | namespace mb_shell { 7 | struct res_string_loader { 8 | struct res_string_identifier { 9 | size_t id; 10 | size_t module; 11 | }; 12 | 13 | using string_id = std::variant; 14 | static string_id string_to_id(std::wstring str); 15 | static std::string string_to_id_string(std::wstring str); 16 | static void init_hook(); 17 | static void init_known_strings(); 18 | 19 | static void init(); 20 | }; 21 | } -------------------------------------------------------------------------------- /src/shell/script/quickjspp.cc: -------------------------------------------------------------------------------- 1 | #include "quickjspp.hpp" 2 | 3 | namespace qjs { 4 | thread_local Context *Context::current; 5 | } -------------------------------------------------------------------------------- /src/shell/script/script.cc: -------------------------------------------------------------------------------- 1 | #include "script.h" 2 | #include "../contextmenu/contextmenu.h" 3 | #include "binding_qjs.h" 4 | 5 | #include "../config.h" 6 | #include "../utils.h" 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | #include "FileWatch.hpp" 20 | #include "quickjs.h" 21 | #include "quickjspp.hpp" 22 | 23 | #include "../logger.h" 24 | 25 | thread_local bool is_thread_js_main = false; 26 | 27 | static unsigned char script_js_bytes[] = { 28 | #include "script.js.h" 29 | }; 30 | 31 | std::string breeze_script_js = std::string( 32 | reinterpret_cast(script_js_bytes), sizeof(script_js_bytes) - 1); 33 | 34 | namespace mb_shell { 35 | 36 | void println(qjs::rest args) { 37 | std::stringstream ss; 38 | for (auto &arg : args) { 39 | ss << arg << " "; 40 | } 41 | ss << std::endl; 42 | auto ws = utf8_to_wstring(ss.str()); 43 | WriteConsoleW(GetStdHandle(STD_OUTPUT_HANDLE), ws.c_str(), ws.size(), nullptr, 44 | nullptr); 45 | } 46 | 47 | void script_context::bind() { 48 | auto &module = js->addModule("mshell"); 49 | 50 | module.function("println", println); 51 | 52 | bindAll(module); 53 | 54 | auto g = js->global(); 55 | g["console"] = js->newObject(); 56 | qjs::Value println_fn = 57 | qjs::js_traits)>>::wrap( 58 | js->ctx, println); 59 | g["console"]["log"] = println_fn; 60 | g["console"]["info"] = println_fn; 61 | g["console"]["warn"] = println_fn; 62 | g["console"]["error"] = println_fn; 63 | g["console"]["debug"] = println_fn; 64 | } 65 | script_context::script_context() : rt{}, js{} {} 66 | void script_context::watch_folder(const std::filesystem::path &path, 67 | std::function on_reload) { 68 | bool has_update = false; 69 | 70 | std::optional js_thread; 71 | auto reload_all = [&]() { 72 | dbgout("Reloading all scripts"); 73 | 74 | *stop_signal = true; 75 | stop_signal = std::make_shared(false); 76 | 77 | if (js_thread) 78 | js_thread->join(); 79 | dbgout("Creating JS thread"); 80 | menu_callbacks_js.clear(); 81 | 82 | js_thread = std::thread([&, this, ss = stop_signal]() { 83 | is_thread_js_main = true; 84 | set_thread_locale_utf8(); 85 | rt = std::make_shared(); 86 | JS_UpdateStackTop(rt->rt); 87 | js = std::make_shared(*rt); 88 | bind(); 89 | try { 90 | JS_UpdateStackTop(rt->rt); 91 | js->eval(breeze_script_js, "breeze-script.js", JS_EVAL_TYPE_MODULE); 92 | } catch (std::exception &e) { 93 | std::cerr << "Error in breeze-script.js: " << e.what() << std::endl; 94 | } 95 | 96 | std::vector files; 97 | std::ranges::copy(std::filesystem::directory_iterator(path) | 98 | std::ranges::views::filter([](auto &entry) { 99 | return entry.path().extension() == ".js"; 100 | }), 101 | std::back_inserter(files)); 102 | 103 | // resort files by config 104 | auto plugin_load_order = config::current->plugin_load_order; 105 | // if not found, load after all 106 | std::ranges::sort(files, [&](auto &a, auto &b) { 107 | auto a_name = a.filename().stem().string(); 108 | auto b_name = b.filename().stem().string(); 109 | 110 | auto a_pos = std::ranges::find(plugin_load_order, a_name); 111 | auto b_pos = std::ranges::find(plugin_load_order, b_name); 112 | 113 | if (a_pos == plugin_load_order.end() && 114 | b_pos == plugin_load_order.end()) { 115 | return a_name < b_name; 116 | } 117 | 118 | if (a_pos == plugin_load_order.end()) { 119 | return false; 120 | } 121 | 122 | if (b_pos == plugin_load_order.end()) { 123 | return true; 124 | } 125 | 126 | return a_pos < b_pos; 127 | }); 128 | 129 | for (auto &path : files) { 130 | try { 131 | std::ifstream file(path); 132 | std::string script((std::istreambuf_iterator(file)), 133 | std::istreambuf_iterator()); 134 | 135 | js->moduleLoader = [&](std::string_view module_name) { 136 | auto module_path = 137 | path.parent_path() / (std::string(module_name) + ".js"); 138 | if (!std::filesystem::exists(module_path)) { 139 | return qjs::Context::ModuleData{}; 140 | } 141 | std::ifstream file(module_path); 142 | std::string script((std::istreambuf_iterator(file)), 143 | std::istreambuf_iterator()); 144 | return qjs::Context::ModuleData{script}; 145 | }; 146 | auto func = JS_Eval(js->ctx, script.c_str(), script.size(), 147 | path.generic_string().c_str(), 148 | JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY); 149 | 150 | if (JS_IsException(func)) { 151 | std::cerr << "Error in file: " << path << std::endl; 152 | JS_FreeValue(js->ctx, func); 153 | continue; 154 | } 155 | 156 | JSModuleDef *m = (JSModuleDef *)JS_VALUE_GET_PTR(func); 157 | auto meta_obj = JS_GetImportMeta(js->ctx, m); 158 | 159 | JS_DefinePropertyValueStr( 160 | js->ctx, meta_obj, "url", 161 | JS_NewString(js->ctx, path.generic_string().c_str()), 162 | JS_PROP_C_W_E); 163 | 164 | JS_DefinePropertyValueStr( 165 | js->ctx, meta_obj, "name", 166 | JS_NewString(js->ctx, path.filename().generic_string().c_str()), 167 | JS_PROP_C_W_E); 168 | 169 | JS_FreeValue(js->ctx, meta_obj); 170 | 171 | JS_EvalFunction(js->ctx, func); 172 | } catch (std::exception &e) { 173 | std::cerr << "Error in file: " << path << " " << e.what() 174 | << std::endl; 175 | } 176 | } 177 | 178 | while (auto ptr = js) { 179 | if (ptr->ctx) { 180 | while (JS_IsJobPending(rt->rt) && !*ss) { 181 | auto ctx = ptr->ctx; 182 | auto ctx1 = ctx; 183 | auto res = JS_ExecutePendingJob(rt->rt, &ctx1); 184 | if (res == -999) { 185 | has_update = true; 186 | dbgout("JS loop critical error! Restarting..."); 187 | return; 188 | } 189 | std::lock_guard lock(ptr->js_job_start_mutex); 190 | ptr->has_pending_job = JS_IsJobPending(rt->rt); 191 | } 192 | } 193 | if (*ss) 194 | break; 195 | std::unique_lock lock(js->js_job_start_mutex); 196 | if (js->has_pending_job) 197 | continue; 198 | js->js_job_start_cv.wait_for( 199 | lock, std::chrono::milliseconds(100), 200 | [&]() { return js->has_pending_job || *ss; }); 201 | } 202 | is_thread_js_main = false; 203 | }); 204 | }; 205 | 206 | reload_all(); 207 | 208 | filewatch::FileWatch watch( 209 | path.generic_string(), 210 | [&](const std::string &path, const filewatch::Event change_type) { 211 | if (!path.ends_with(".js")) { 212 | return; 213 | } 214 | 215 | dbgout("File change detected: {}", path); 216 | has_update = true; 217 | }); 218 | 219 | while (true) { 220 | std::this_thread::sleep_for(std::chrono::milliseconds(300)); 221 | if (has_update && on_reload()) { 222 | has_update = false; 223 | reload_all(); 224 | } 225 | } 226 | } 227 | 228 | } // namespace mb_shell 229 | -------------------------------------------------------------------------------- /src/shell/script/script.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "quickjspp.hpp" 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | extern thread_local bool is_thread_js_main; 14 | namespace mb_shell { 15 | struct script_context { 16 | std::shared_ptr rt; 17 | std::shared_ptr js; 18 | std::shared_ptr stop_signal = std::make_shared(0); 19 | script_context(); 20 | void bind(); 21 | 22 | void watch_folder( 23 | const std::filesystem::path &path, 24 | std::function on_reload = []() { return true; }); 25 | }; 26 | } // namespace mb_shell -------------------------------------------------------------------------------- /src/shell/utils.cc: -------------------------------------------------------------------------------- 1 | #include "utils.h" 2 | #define _SILENCE_ALL_CXX17_DEPRECATION_WARNINGS 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | 10 | #include 11 | 12 | #include "windows.h" 13 | #include 14 | 15 | #include "logger.h" 16 | 17 | std::wstring mb_shell::utf8_to_wstring(std::string const &str) { 18 | std::wstring_convert< 19 | std::conditional_t, 20 | std::codecvt_utf8_utf16>> 21 | converter; 22 | return converter.from_bytes(str); 23 | } 24 | std::string mb_shell::wstring_to_utf8(std::wstring const &str) { 25 | std::wstring_convert< 26 | std::conditional_t, 27 | std::codecvt_utf8_utf16>> 28 | converter; 29 | return converter.to_bytes(str); 30 | } 31 | 32 | bool mb_shell::is_win11_or_later() { 33 | using rtl_get_nt_version_numbers = 34 | void(NTAPI *)(uint32_t *, uint32_t *, uint32_t *); 35 | uint32_t major, minor, build; 36 | 37 | auto ntdll = GetModuleHandleW(L"ntdll.dll"); 38 | if (!ntdll) { 39 | return false; 40 | } 41 | 42 | auto RtlGetNtVersionNumbers = (rtl_get_nt_version_numbers)GetProcAddress( 43 | ntdll, "RtlGetNtVersionNumbers"); 44 | 45 | if (!RtlGetNtVersionNumbers) { 46 | return false; 47 | } 48 | 49 | RtlGetNtVersionNumbers(&major, &minor, &build); 50 | build &= 0xFFFF; 51 | return (major >= 10 && build >= 22000); 52 | } 53 | bool get_personalize_dword_value(const wchar_t *value_name) { 54 | auto key = HKEY_CURRENT_USER; 55 | auto subkey = 56 | L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; 57 | DWORD data = 0; 58 | DWORD size = sizeof(data); 59 | auto res = RegGetValueW(key, subkey, value_name, RRF_RT_REG_DWORD, nullptr, 60 | &data, &size); 61 | return res == ERROR_SUCCESS && data == 1; 62 | } 63 | 64 | bool mb_shell::is_light_mode() { 65 | return get_personalize_dword_value(L"AppsUseLightTheme"); 66 | } 67 | 68 | bool mb_shell::is_acrylic_available() { 69 | return get_personalize_dword_value(L"EnableTransparency"); 70 | } 71 | std::optional mb_shell::env(const std::string &name) { 72 | wchar_t buffer[32767]; 73 | GetEnvironmentVariableW(utf8_to_wstring(name).c_str(), buffer, 32767); 74 | if (buffer[0] == 0) { 75 | return std::nullopt; 76 | } 77 | return wstring_to_utf8(buffer); 78 | } 79 | bool mb_shell::is_memory_readable(const void *ptr) { 80 | MEMORY_BASIC_INFORMATION mbi; 81 | if (!VirtualQuery(ptr, &mbi, sizeof(mbi))) { 82 | return false; 83 | } 84 | DWORD mask = PAGE_READONLY | PAGE_READWRITE | PAGE_WRITECOPY | 85 | PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE | 86 | PAGE_EXECUTE_WRITECOPY; 87 | return (mbi.Protect & mask) != 0; 88 | } 89 | NVGcolor mb_shell::parse_color(const std::string &str) { 90 | // allowed: 91 | // #RRGGBB 92 | // #RRGGBBAA 93 | // #RGB 94 | // #RGBA 95 | // RRGGBB 96 | // RRGGBBAA 97 | // RGB 98 | // RGBA 99 | // r, g,b 100 | // r,g, b,a 101 | 102 | std::string s = str; 103 | if (s.empty()) 104 | return nvgRGBA(0, 0, 0, 255); 105 | 106 | // Remove leading '#' if present 107 | if (s[0] == '#') 108 | s = s.substr(1); 109 | 110 | // Handle comma-separated values 111 | if (s.find(',') != std::string::npos) { 112 | std::vector components; 113 | std::stringstream ss(s); 114 | std::string item; 115 | while (std::getline(ss, item, ',')) { 116 | components.push_back(std::stoi(item)); 117 | } 118 | 119 | if (components.size() == 3) { 120 | return nvgRGB(components[0], components[1], components[2]); 121 | } 122 | if (components.size() == 4) { 123 | return nvgRGBA(components[0], components[1], components[2], 124 | components[3]); 125 | } 126 | } 127 | 128 | // Handle hex values 129 | switch (s.length()) { 130 | case 3: // RGB 131 | return nvgRGB(17 * std::stoi(s.substr(0, 1), nullptr, 16), 132 | 17 * std::stoi(s.substr(1, 1), nullptr, 16), 133 | 17 * std::stoi(s.substr(2, 1), nullptr, 16)); 134 | case 4: // RGBA 135 | return nvgRGBA(17 * std::stoi(s.substr(0, 1), nullptr, 16), 136 | 17 * std::stoi(s.substr(1, 1), nullptr, 16), 137 | 17 * std::stoi(s.substr(2, 1), nullptr, 16), 138 | 17 * std::stoi(s.substr(3, 1), nullptr, 16)); 139 | case 6: // RRGGBB 140 | return nvgRGB(std::stoi(s.substr(0, 2), nullptr, 16), 141 | std::stoi(s.substr(2, 2), nullptr, 16), 142 | std::stoi(s.substr(4, 2), nullptr, 16)); 143 | case 8: // RRGGBBAA 144 | return nvgRGBA(std::stoi(s.substr(0, 2), nullptr, 16), 145 | std::stoi(s.substr(2, 2), nullptr, 16), 146 | std::stoi(s.substr(4, 2), nullptr, 16), 147 | std::stoi(s.substr(6, 2), nullptr, 16)); 148 | } 149 | 150 | return nvgRGBA(0, 0, 0, 255); // Default black 151 | } 152 | void mb_shell::set_thread_locale_utf8() { 153 | std::setlocale(LC_CTYPE, ".UTF-8"); 154 | std::locale::global(std::locale("en_US.UTF-8")); 155 | SetConsoleOutputCP(CP_UTF8); 156 | SetConsoleCP(CP_UTF8); 157 | 158 | SetThreadLocale( 159 | MAKELCID(MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US), SORT_DEFAULT)); 160 | } 161 | mb_shell::task_queue::task_queue() : stop(false) { 162 | worker = std::thread(&task_queue::run, this); 163 | } 164 | mb_shell::task_queue::~task_queue() { 165 | { 166 | std::lock_guard lock(queue_mutex); 167 | stop = true; 168 | } 169 | condition.notify_all(); 170 | if (worker.joinable()) { 171 | worker.join(); 172 | } 173 | } 174 | void mb_shell::task_queue::run() { 175 | while (true) { 176 | std::function task; 177 | { 178 | std::unique_lock lock(queue_mutex); 179 | condition.wait(lock, [this]() { return stop || !tasks.empty(); }); 180 | 181 | if (stop && tasks.empty()) { 182 | return; 183 | } 184 | 185 | task = std::move(tasks.front()); 186 | tasks.pop(); 187 | } 188 | 189 | task(); 190 | } 191 | } 192 | mb_shell::perf_counter::perf_counter(std::string name) : name(name) { 193 | start = std::chrono::high_resolution_clock::now(); 194 | last_end = start; 195 | } 196 | void mb_shell::perf_counter::end(std::optional block_name) { 197 | auto now = std::chrono::high_resolution_clock::now(); 198 | 199 | if (block_name) { 200 | dbgout( 201 | "[perf] {}: {}ms / {} {}ms", block_name.value(), 202 | std::chrono::duration_cast(now - last_end) 203 | .count(), 204 | name, 205 | std::chrono::duration_cast(now - start) 206 | .count()); 207 | } else { 208 | dbgout( 209 | "[perf] {}: {}ms", name, 210 | std::chrono::duration_cast(now - start) 211 | .count()); 212 | } 213 | last_end = now; 214 | } 215 | -------------------------------------------------------------------------------- /src/shell/utils.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "nanovg.h" 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | namespace mb_shell { 16 | std::string wstring_to_utf8(std::wstring const &str); 17 | std::wstring utf8_to_wstring(std::string const &str); 18 | bool is_win11_or_later(); 19 | bool is_light_mode(); 20 | bool is_acrylic_available(); 21 | std::optional env(const std::string &name); 22 | bool is_memory_readable(const void *ptr); 23 | NVGcolor parse_color(const std::string &str); 24 | void set_thread_locale_utf8(); 25 | 26 | struct task_queue { 27 | public: 28 | task_queue(); 29 | 30 | ~task_queue(); 31 | 32 | template 33 | auto add_task(F &&f, Args &&...args) 34 | -> std::future> { 35 | using return_type = std::invoke_result_t; 36 | 37 | if (stop) { 38 | throw std::runtime_error("add_task called on stopped task_queue"); 39 | } 40 | 41 | auto task = std::make_shared>( 42 | std::bind(std::forward(f), std::forward(args)...)); 43 | 44 | std::future res = task->get_future(); 45 | 46 | { 47 | std::lock_guard lock(queue_mutex); 48 | tasks.emplace([task]() { (*task)(); }); 49 | } 50 | 51 | condition.notify_one(); 52 | return res; 53 | } 54 | 55 | private: 56 | void run(); 57 | 58 | std::thread worker; 59 | std::queue> tasks; 60 | std::mutex queue_mutex; 61 | std::condition_variable condition; 62 | bool stop; 63 | }; 64 | 65 | struct perf_counter { 66 | std::chrono::high_resolution_clock::time_point start; 67 | std::chrono::high_resolution_clock::time_point last_end; 68 | std::string name; 69 | void end(std::optional block_name = {}); 70 | perf_counter(std::string name); 71 | }; 72 | } // namespace mb_shell -------------------------------------------------------------------------------- /src/shell/window_proc_hook.cc: -------------------------------------------------------------------------------- 1 | #include "window_proc_hook.h" 2 | #include "blook/blook.h" 3 | 4 | #include 5 | #include 6 | 7 | namespace mb_shell { 8 | static std::unordered_set hooked_windows; 9 | 10 | void window_proc_hook::install(void *hwnd) { 11 | if (installed) 12 | return; 13 | this->hwnd = hwnd; 14 | this->original_proc = (void *)GetWindowLongPtrW((HWND)hwnd, GWLP_WNDPROC); 15 | 16 | this->hooked_proc = (void *)blook::Function::into_function_pointer( 17 | [this](HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) -> LRESULT { 18 | SetWindowLongPtrW((HWND)hwnd, GWLP_WNDPROC, 19 | (LONG_PTR)this->original_proc); 20 | 21 | for (auto &f : this->hooks) { 22 | f(hwnd, this->original_proc, msg, wp, lp); 23 | } 24 | 25 | while (!this->tasks.empty()) { 26 | this->tasks.front()(); 27 | this->tasks.pop(); 28 | } 29 | 30 | SetWindowLongPtrW((HWND)hwnd, GWLP_WNDPROC, 31 | (LONG_PTR)this->hooked_proc); 32 | 33 | return CallWindowProcW((WNDPROC)this->original_proc, hwnd, msg, wp, lp); 34 | }); 35 | 36 | SetWindowLongPtrW((HWND)hwnd, GWLP_WNDPROC, (LONG_PTR)this->hooked_proc); 37 | installed = true; 38 | } 39 | 40 | void window_proc_hook::uninstall() { 41 | SetWindowLongPtrW((HWND)hwnd, GWLP_WNDPROC, (LONG_PTR)original_proc); 42 | installed = false; 43 | } 44 | window_proc_hook::~window_proc_hook() { 45 | if (installed) { 46 | uninstall(); 47 | } 48 | } 49 | } // namespace mb_shell -------------------------------------------------------------------------------- /src/shell/window_proc_hook.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | namespace mb_shell { 9 | struct window_proc_hook { 10 | void* hwnd; 11 | void* original_proc; 12 | void* hooked_proc = nullptr; 13 | bool installed = false; 14 | 15 | std::vector> hooks; 16 | std::queue> tasks; 17 | 18 | auto add_task(auto&& f) -> std::future> { 19 | using return_type = std::invoke_result_t; 20 | auto task = std::make_shared>(std::forward(f)); 21 | std::future res = task->get_future(); 22 | tasks.emplace([task]() { (*task)(); }); 23 | return res; 24 | } 25 | 26 | void install(void* hwnd); 27 | void uninstall(); 28 | ~window_proc_hook(); 29 | }; 30 | } -------------------------------------------------------------------------------- /src/ui/animator.cc: -------------------------------------------------------------------------------- 1 | #include "animator.h" 2 | #include "widget.h" 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | void ui::animated_float::update(float delta_t) { 9 | if (easing == easing_type::mutation) { 10 | if (destination != value) { 11 | value = destination; 12 | if (after_animate) 13 | after_animate.value()(destination); 14 | _updated = true; 15 | } else { 16 | _updated = false; 17 | } 18 | return; 19 | } 20 | 21 | if (delay_timer < delay) { 22 | delay_timer += delta_t; 23 | _updated = false; 24 | return; 25 | } 26 | 27 | progress += delta_t / duration; 28 | 29 | if (progress < 0.f) { 30 | _updated = false; 31 | return; 32 | } 33 | 34 | if (progress >= 1.f) { 35 | progress = 1.f; 36 | if (value != destination) { 37 | value = destination; 38 | _updated = true; 39 | if (after_animate) { 40 | after_animate.value()(destination); 41 | } 42 | } else { 43 | _updated = false; 44 | } 45 | return; 46 | } 47 | 48 | if (easing == easing_type::linear) { 49 | value = std::lerp(from, destination, progress); 50 | } else if (easing == easing_type::ease_in) { 51 | value = std::lerp(from, destination, progress * progress); 52 | } else if (easing == easing_type::ease_out) { 53 | value = std::lerp(from, destination, 1 - std::sqrt(1 - progress)); 54 | } else if (easing == easing_type::ease_in_out) { 55 | value = std::lerp( 56 | from, destination, 57 | (0.5f * std::sin(progress * std::numbers::pi - std::numbers::pi / 2) + 58 | 0.5f)); 59 | } 60 | 61 | _updated = true; 62 | } 63 | void ui::animated_float::animate_to(float dest) { 64 | if (this->destination == dest) 65 | return; 66 | this->from = value; 67 | this->destination = dest; 68 | progress = 0.f; 69 | delay_timer = 0.f; 70 | 71 | if (before_animate) { 72 | before_animate.value()(dest); 73 | } 74 | } 75 | float ui::animated_float::var() const { return value; } 76 | float ui::animated_float::prog() const { return progress; } 77 | float ui::animated_float::dest() const { 78 | return destination; 79 | } 80 | void ui::animated_float::reset_to(float dest) { 81 | if (value != dest) 82 | _updated = true; 83 | value = dest; 84 | this->from = dest; 85 | this->destination = dest; 86 | progress = 1.f; 87 | delay_timer = 0.f; 88 | } 89 | void ui::animated_float::set_easing(easing_type easing) { 90 | this->easing = easing; 91 | } 92 | void ui::animated_float::set_duration(float duration) { 93 | this->duration = duration; 94 | } 95 | bool ui::animated_float::updated() const { return _updated; } 96 | void ui::animated_float::set_delay(float delay) { 97 | this->delay = delay; 98 | delay_timer = 0.f; 99 | } 100 | std::array ui::animated_color::operator*() const { 101 | return {r->var(), g->var(), b->var(), a->var()}; 102 | } 103 | ui::animated_color::animated_color(ui::widget *thiz, float r, float g, float b, 104 | float a) 105 | : r(thiz->anim_float(r)), g(thiz->anim_float(g)), b(thiz->anim_float(b)), 106 | a(thiz->anim_float(a)) {} 107 | -------------------------------------------------------------------------------- /src/ui/animator.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "nanovg.h" 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | 10 | namespace ui { 11 | struct widget; 12 | enum class easing_type { 13 | mutation, 14 | linear, 15 | ease_in, 16 | ease_out, 17 | ease_in_out, 18 | }; 19 | struct animated_float { 20 | animated_float() = default; 21 | animated_float(animated_float &&) = default; 22 | animated_float &operator=(animated_float &&) = default; 23 | animated_float(const animated_float &) = delete; 24 | animated_float &operator=(const animated_float &) = delete; 25 | 26 | animated_float(float destination, float duration = 200.f, 27 | easing_type easing = easing_type::mutation) 28 | : easing(easing), duration(duration), destination(destination) {} 29 | 30 | std::optional> before_animate = {}; 31 | std::optional> after_animate = {}; 32 | 33 | operator float() const { return var(); } 34 | float operator*() const { return var(); } 35 | void update(float delta_t); 36 | 37 | void animate_to(float destination); 38 | void reset_to(float destination); 39 | void set_duration(float duration); 40 | void set_easing(easing_type easing); 41 | void set_delay(float delay); 42 | // current value 43 | float var() const; 44 | // progress, if have any 45 | float prog() const; 46 | float dest() const; 47 | bool updated() const; 48 | 49 | easing_type easing = easing_type::mutation; 50 | float progress = 0.f; 51 | 52 | private: 53 | float duration = 200.f; 54 | float value = 0.f; 55 | float from = 0.f; 56 | float destination = value; 57 | float delay = 0.f, delay_timer = 0.f; 58 | bool _updated = true; 59 | }; 60 | 61 | using sp_anim_float = std::shared_ptr; 62 | 63 | struct animated_color { 64 | sp_anim_float r = nullptr; 65 | sp_anim_float g = nullptr; 66 | sp_anim_float b = nullptr; 67 | sp_anim_float a = nullptr; 68 | 69 | animated_color() = delete; 70 | animated_color(animated_color &&) = default; 71 | 72 | animated_color(ui::widget *thiz, float r = 0, float g = 0, float b = 0, 73 | float a = 0); 74 | 75 | std::array operator*() const; 76 | 77 | inline void animate_to(float r, float g, float b, float a) { 78 | this->r->animate_to(r); 79 | this->g->animate_to(g); 80 | this->b->animate_to(b); 81 | this->a->animate_to(a); 82 | } 83 | 84 | inline void animate_to(const std::array &color) { 85 | animate_to(color[0], color[1], color[2], color[3]); 86 | } 87 | 88 | inline void reset_to(float r, float g, float b, float a) { 89 | this->r->reset_to(r); 90 | this->g->reset_to(g); 91 | this->b->reset_to(b); 92 | this->a->reset_to(a); 93 | } 94 | 95 | inline void reset_to(const std::array &color) { 96 | reset_to(color[0], color[1], color[2], color[3]); 97 | } 98 | 99 | inline NVGcolor nvg() const { 100 | return nvgRGBAf(r->var(), g->var(), b->var(), a->var()); 101 | } 102 | }; 103 | } // namespace ui -------------------------------------------------------------------------------- /src/ui/extra_widgets.cc: -------------------------------------------------------------------------------- 1 | #include "extra_widgets.h" 2 | #include "widget.h" 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "swcadef.h" 9 | #include "ui.h" 10 | #define GLFW_EXPOSE_NATIVE_WIN32 11 | #include "GLFW/glfw3.h" 12 | #include "GLFW/glfw3native.h" 13 | LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { 14 | if (msg == WM_MOUSEACTIVATE) { 15 | return MA_NOACTIVATE; 16 | } else if (msg == WM_PAINT) { 17 | PAINTSTRUCT ps; 18 | BeginPaint(hwnd, &ps); 19 | EndPaint(hwnd, &ps); 20 | return 0; 21 | } 22 | return DefWindowProc(hwnd, msg, wParam, lParam); 23 | } 24 | 25 | int GetWindowZOrder(HWND hwnd) { 26 | int z = 1; 27 | for (HWND h = hwnd; h; h = GetWindow(h, GW_HWNDNEXT)) { 28 | z++; 29 | } 30 | return z; 31 | } 32 | 33 | namespace ui { 34 | void acrylic_background_widget::update(update_context &ctx) { 35 | if (!render_thread) { 36 | auto win = glfwGetCurrentContext(); 37 | if (!win) { 38 | std::cerr << "[acrylic window] Failed to get current context" 39 | << std::endl; 40 | return; 41 | } 42 | auto handle = glfwGetWin32Window(win); 43 | render_thread = std::thread([=, this]() { 44 | hwnd = 45 | CreateWindowExW(WS_EX_TOOLWINDOW | WS_EX_TRANSPARENT | 46 | WS_EX_NOACTIVATE | WS_EX_LAYERED | WS_EX_TOPMOST, 47 | L"mbui-acrylic-bg", L"", WS_POPUP, *x, *y, 0, 0, 48 | nullptr, NULL, GetModuleHandleW(nullptr), NULL); 49 | 50 | if (!hwnd) { 51 | std::printf("Failed to create window %d\n", GetLastError()); 52 | } 53 | 54 | SetWindowLongPtrW((HWND)hwnd, GWLP_WNDPROC, (LONG_PTR)WndProc); 55 | 56 | update_color(); 57 | 58 | if (use_dwm) { 59 | // dwm round corners 60 | auto round_value = radius > 0 ? DWMWCP_ROUND : DWMWCP_DONOTROUND; 61 | DwmSetWindowAttribute((HWND)hwnd, DWMWA_WINDOW_CORNER_PREFERENCE, 62 | &round_value, sizeof(round_value)); 63 | } 64 | 65 | std::unique_lock lk(cv_m); 66 | 67 | ShowWindow((HWND)hwnd, SW_SHOW); 68 | 69 | SetWindowPos((HWND)hwnd, handle, 0, 0, 0, 0, 70 | SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_NOREDRAW | 71 | SWP_NOSENDCHANGING | SWP_NOCOPYBITS); 72 | 73 | bool rgn_set = false; 74 | while (true) { 75 | 76 | if (to_close) { 77 | ShowWindow((HWND)hwnd, SW_HIDE); 78 | DestroyWindow((HWND)hwnd); 79 | break; 80 | } 81 | 82 | int winx, winy; 83 | RECT rect; 84 | GetWindowRect(handle, &rect); 85 | winx = rect.left; 86 | winy = rect.top; 87 | 88 | SetWindowPos((HWND)hwnd, nullptr, winx + (*x + offset_x) * dpi_scale, 89 | winy + (*y + offset_y) * dpi_scale, *width * dpi_scale, 90 | *height * dpi_scale, 91 | SWP_NOACTIVATE | SWP_NOREDRAW | SWP_NOOWNERZORDER | 92 | SWP_NOSENDCHANGING | SWP_NOCOPYBITS | 93 | SWP_NOREPOSITION | SWP_NOZORDER); 94 | 95 | auto zorder_this = GetWindowZOrder((HWND)hwnd); 96 | auto zorder_last = GetWindowZOrder((HWND)last_hwnd_self); 97 | 98 | if (zorder_this < zorder_last && last_hwnd_self && hwnd) { 99 | SetWindowPos((HWND)hwnd, handle, 0, 0, 0, 0, 100 | SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_NOREDRAW | 101 | SWP_NOSENDCHANGING | SWP_NOCOPYBITS); 102 | } 103 | 104 | SetLayeredWindowAttributes((HWND)hwnd, 0, *opacity, LWA_ALPHA); 105 | 106 | if ((width->updated() || height->updated() || radius->updated() || 107 | !rgn_set) && 108 | !use_dwm) { 109 | rgn_set = true; 110 | auto rgn = CreateRoundRectRgn( 111 | 0, 0, *width * dpi_scale, *height * dpi_scale, 112 | *radius * 2 * dpi_scale, *radius * 2 * dpi_scale); 113 | SetWindowRgn((HWND)hwnd, rgn, 1); 114 | 115 | if (rgn) { 116 | DeleteObject(rgn); 117 | } 118 | } 119 | 120 | cv.wait_for(lk, std::chrono::milliseconds(200)); 121 | 122 | // limit to 60fps 123 | std::this_thread::sleep_for(std::chrono::milliseconds(16)); 124 | } 125 | }); 126 | } 127 | 128 | rect_widget::update(ctx); 129 | dpi_scale = ctx.rt.dpi_scale; 130 | cv.notify_all(); 131 | last_hwnd = nullptr; 132 | if (use_dwm) { 133 | radius->reset_to(8.f); 134 | } 135 | } 136 | thread_local void *acrylic_background_widget::last_hwnd = 0; 137 | void acrylic_background_widget::render(nanovg_context ctx) { 138 | widget::render(ctx); 139 | cv.notify_all(); 140 | 141 | auto bg_color_tmp = bg_color; 142 | bg_color_tmp.a *= *opacity / 255.f; 143 | ctx.fillColor(bg_color_tmp); 144 | ctx.fillRoundedRect(*x, *y, *width, *height, *radius); 145 | 146 | last_hwnd_self = last_hwnd; 147 | last_hwnd = hwnd; 148 | 149 | offset_x = ctx.offset_x; 150 | offset_y = ctx.offset_y; 151 | } 152 | 153 | acrylic_background_widget::acrylic_background_widget(bool use_dwm) 154 | : rect_widget(), use_dwm(use_dwm) { 155 | static bool registered = false; 156 | if (!registered) { 157 | WNDCLASSW wc = {0}; 158 | wc.lpfnWndProc = WndProc; 159 | wc.hInstance = GetModuleHandleW(nullptr); 160 | wc.lpszClassName = L"mbui-acrylic-bg"; 161 | RegisterClassW(&wc); 162 | registered = true; 163 | } 164 | } 165 | acrylic_background_widget::~acrylic_background_widget() { 166 | to_close = true; 167 | cv.notify_all(); 168 | if (render_thread) 169 | render_thread->join(); 170 | } 171 | 172 | // b a r g 173 | // r g b a 174 | 175 | void acrylic_background_widget::update_color() { 176 | ACCENT_POLICY accent = { 177 | ACCENT_ENABLE_ACRYLICBLURBEHIND, 178 | Flags::GradientColor | Flags::AllBorder | Flags::AllowSetWindowRgn, 179 | // GradientColor uses BGRA 180 | ARGB(acrylic_bg_color.a * 255, acrylic_bg_color.b * 255, 181 | acrylic_bg_color.g * 255, acrylic_bg_color.r * 255), 182 | 0}; 183 | WINDOWCOMPOSITIONATTRIBDATA data = {WCA_ACCENT_POLICY, &accent, 184 | sizeof(accent)}; 185 | pSetWindowCompositionAttribute((HWND)hwnd, &data); 186 | } 187 | void rect_widget::render(nanovg_context ctx) { 188 | bg_color.a = *opacity / 255.f; 189 | ctx.fillColor(bg_color); 190 | ctx.fillRoundedRect(*x, *y, *width, *height, *radius); 191 | } 192 | rect_widget::rect_widget() : widget() {} 193 | rect_widget::~rect_widget() {} 194 | } // namespace ui -------------------------------------------------------------------------------- /src/ui/extra_widgets.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "animator.h" 3 | #include "nanovg.h" 4 | #include "widget.h" 5 | #include 6 | #include 7 | #include 8 | 9 | namespace ui { 10 | 11 | struct rect_widget : public widget { 12 | rect_widget(); 13 | ~rect_widget(); 14 | sp_anim_float opacity = anim_float(0, 200); 15 | sp_anim_float radius = anim_float(0, 0); 16 | 17 | NVGcolor bg_color = nvgRGBAf(0, 0, 0, 0); 18 | 19 | void render(nanovg_context ctx) override; 20 | }; 21 | 22 | struct acrylic_background_widget : public rect_widget { 23 | void *hwnd = nullptr; 24 | acrylic_background_widget(bool use_dwm = true); 25 | ~acrylic_background_widget(); 26 | bool use_dwm = true; 27 | NVGcolor acrylic_bg_color = nvgRGBAf(1, 0, 0, 0); 28 | std::optional render_thread; 29 | std::condition_variable cv; 30 | std::mutex cv_m; 31 | bool to_close = false; 32 | float offset_x = 0, offset_y = 0, dpi_scale = 1; 33 | static thread_local void* last_hwnd; 34 | void* last_hwnd_self = nullptr; 35 | void update_color(); 36 | 37 | void render(nanovg_context ctx) override; 38 | 39 | void update(update_context &ctx) override; 40 | }; 41 | } // namespace ui -------------------------------------------------------------------------------- /src/ui/hbitmap_utils.cc: -------------------------------------------------------------------------------- 1 | #include "hbitmap_utils.h" 2 | #include "Windows.h" 3 | #include 4 | #include 5 | ui::NVGImage ui::LoadBitmapImage(nanovg_context ctx, void *hbitmap) { 6 | HBITMAP hBitmap = (HBITMAP)hbitmap; 7 | BITMAP bm; 8 | 9 | auto dc = CreateCompatibleDC(NULL); 10 | 11 | GetObject(hBitmap, sizeof(bm), &bm); 12 | 13 | BITMAPINFO bi = {0}; 14 | bi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); 15 | 16 | if (!GetDIBits(dc, hBitmap, 0, 0, NULL, &bi, DIB_RGB_COLORS)) { 17 | printf("Failed to get DIB bits\n"); 18 | return NVGImage(-1, 0, 0, ctx); 19 | } 20 | 21 | std::vector bits(bi.bmiHeader.biSizeImage); 22 | 23 | bi.bmiHeader.biBitCount = 32; 24 | bi.bmiHeader.biCompression = BI_RGB; 25 | 26 | if (!GetDIBits(dc, hBitmap, 0, bm.bmHeight, bits.data(), &bi, 27 | DIB_RGB_COLORS)) { 28 | printf("Failed to get DIB bits\n"); 29 | return NVGImage(-1, 0, 0, ctx); 30 | } 31 | 32 | DeleteDC(dc); 33 | 34 | std::vector rgba(bm.bmWidth * bm.bmHeight * 4); 35 | 36 | for (int i = 0; i < bm.bmWidth * bm.bmHeight; i++) { 37 | rgba[i * 4 + 0] = bits[i * 4 + 2]; 38 | rgba[i * 4 + 1] = bits[i * 4 + 1]; 39 | rgba[i * 4 + 2] = bits[i * 4 + 0]; 40 | rgba[i * 4 + 3] = bits[i * 4 + 3]; 41 | } 42 | 43 | // hbitmap is in reverse order (bottom to top) 44 | for (int y = 0; y < bm.bmHeight / 2; y++) { 45 | for (int x = 0; x < bm.bmWidth; x++) { 46 | for (int i = 0; i < 4; i++) { 47 | std::swap(rgba[(y * bm.bmWidth + x) * 4 + i], 48 | rgba[((bm.bmHeight - y - 1) * bm.bmWidth + x) * 4 + i]); 49 | } 50 | } 51 | } 52 | 53 | auto id = ctx.createImageRGBA(bm.bmWidth, bm.bmHeight, 0, rgba.data()); 54 | if (id == -1) { 55 | printf("Failed to create image\n"); 56 | return NVGImage(-1, 0, 0, ctx); 57 | } 58 | return NVGImage(id, bm.bmWidth, bm.bmHeight, ctx); 59 | } 60 | -------------------------------------------------------------------------------- /src/ui/hbitmap_utils.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "glad/glad.h" 3 | #include "nanovg_wrapper.h" 4 | #include "widget.h" 5 | namespace ui { 6 | NVGImage LoadBitmapImage(nanovg_context ctx, void* hbitmap); 7 | }; -------------------------------------------------------------------------------- /src/ui/nanosvg.cc: -------------------------------------------------------------------------------- 1 | #define NANOSVG_ALL_COLOR_KEYWORDS 2 | #define NANOSVG_IMPLEMENTATION 3 | #include "nanosvg.h" 4 | #define NANOSVGRAST_IMPLEMENTATION 5 | #include "nanosvgrast.h" -------------------------------------------------------------------------------- /src/ui/swcadef.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #define NOMINMAX 3 | #include "windows.h" 4 | #include 5 | 6 | // Values and names extracted from twinui.pdb (April 2018 update), documented by 7 | // toying with the API. Output from DIA2Dump above the definitions. 8 | 9 | // Enum : ACCENT_STATE, Type: int 10 | // Data : constant 0x0, Constant, Type: int, ACCENT_DISABLED 11 | // Data : constant 0x1, Constant, Type: int, ACCENT_ENABLE_GRADIENT 12 | // Data : constant 0x2, Constant, Type: int, 13 | // ACCENT_ENABLE_TRANSPARENTGRADIENT Data : constant 0x3, Constant, 14 | // Type: int, ACCENT_ENABLE_BLURBEHIND Data : constant 0x4, 15 | // Constant, Type: int, ACCENT_ENABLE_ACRYLICBLURBEHIND Data : 16 | // constant 0x5, Constant, Type: int, ACCENT_ENABLE_HOSTBACKDROP Data : constant 17 | // 0x6, Constant, Type: int, ACCENT_INVALID_STATE 18 | enum ACCENT_STATE : INT { // Affects the rendering of the background of a 19 | // window. 20 | ACCENT_DISABLED = 0, // Default value. Background is black. 21 | ACCENT_ENABLE_GRADIENT = 22 | 1, // Background is GradientColor, alpha channel ignored. 23 | ACCENT_ENABLE_TRANSPARENTGRADIENT = 2, // Background is GradientColor. 24 | ACCENT_ENABLE_BLURBEHIND = 25 | 3, // Background is GradientColor, with blur effect. 26 | ACCENT_ENABLE_ACRYLICBLURBEHIND = 27 | 4, // Background is GradientColor, with acrylic blur effect. 28 | ACCENT_ENABLE_HOSTBACKDROP = 5, // Unknown. 29 | ACCENT_INVALID_STATE = 30 | 6 // Unknown. Seems to draw background fully transparent. 31 | }; 32 | 33 | enum Flags : UINT { 34 | NoneBorder = 0x00, 35 | GradientColor = 0x02, 36 | Luminosity = 0x02, 37 | LeftBorder = 0x20, 38 | TopBorder = 0x40, 39 | RightBorder = 0x80, 40 | BottomBorder = 0x100, 41 | AllBorder = (LeftBorder | TopBorder | RightBorder | BottomBorder), 42 | 43 | AllowSetWindowRgn = 0x10, // 0x10, 0xFE; 16, 25, 140 44 | FullScreen = 0xFF, 45 | FullScreen1 = 0xFFFFFFFF 46 | }; 47 | 48 | inline constexpr DWORD ARGB(DWORD a, DWORD r, DWORD g, DWORD b) { 49 | return (a << 24) | (r << 16) | (g << 8) | b; 50 | } 51 | 52 | // UserDefinedType: ACCENT_POLICY 53 | // Data : this+0x0, Member, Type: enum ACCENT_STATE, AccentState 54 | // Data : this+0x4, Member, Type: unsigned int, AccentFlags 55 | // Data : this+0x8, Member, Type: unsigned long, GradientColor 56 | // Data : this+0xC, Member, Type: long, AnimationId 57 | struct ACCENT_POLICY { // Determines how a window's background is rendered. 58 | ACCENT_STATE AccentState; // Background effect. 59 | UINT AccentFlags; // Flags. Set to 2 to tell GradientColor is used, rest is 60 | // unknown. 61 | COLORREF GradientColor; // Background color. 62 | LONG AnimationId; // Unknown 63 | }; 64 | 65 | // Enum : WINDOWCOMPOSITIONATTRIB, Type: int 66 | // Data : constant 0x0, Constant, Type: int, WCA_UNDEFINED 67 | // Data : constant 0x1, Constant, Type: int, WCA_NCRENDERING_ENABLED 68 | // Data : constant 0x2, Constant, Type: int, WCA_NCRENDERING_POLICY 69 | // Data : constant 0x3, Constant, Type: int, 70 | // WCA_TRANSITIONS_FORCEDISABLED Data : constant 0x4, Constant, 71 | // Type: int, WCA_ALLOW_NCPAINT Data : constant 0x5, Constant, Type: 72 | // int, WCA_CAPTION_BUTTON_BOUNDS Data : constant 0x6, Constant, 73 | // Type: int, WCA_NONCLIENT_RTL_LAYOUT Data : constant 0x7, 74 | // Constant, Type: int, WCA_FORCE_ICONIC_REPRESENTATION Data : 75 | // constant 0x8, Constant, Type: int, WCA_EXTENDED_FRAME_BOUNDS Data : 76 | // constant 0x9, Constant, Type: int, WCA_HAS_ICONIC_BITMAP Data : 77 | // constant 0xA, Constant, Type: int, WCA_THEME_ATTRIBUTES Data : 78 | // constant 0xB, Constant, Type: int, WCA_NCRENDERING_EXILED Data : 79 | // constant 0xC, Constant, Type: int, WCA_NCADORNMENTINFO Data : 80 | // constant 0xD, Constant, Type: int, WCA_EXCLUDED_FROM_LIVEPREVIEW Data : 81 | // constant 0xE, Constant, Type: int, WCA_VIDEO_OVERLAY_ACTIVE Data : 82 | // constant 0xF, Constant, Type: int, WCA_FORCE_ACTIVEWINDOW_APPEARANCE Data : 83 | // constant 0x10, Constant, Type: int, WCA_DISALLOW_PEEK Data : 84 | // constant 0x11, Constant, Type: int, WCA_CLOAK Data : constant 85 | // 0x12, Constant, Type: int, WCA_CLOAKED Data : constant 0x13, 86 | // Constant, Type: int, WCA_ACCENT_POLICY Data : constant 0x14, 87 | // Constant, Type: int, WCA_FREEZE_REPRESENTATION Data : constant 88 | // 0x15, Constant, Type: int, WCA_EVER_UNCLOAKED Data : constant 89 | // 0x16, Constant, Type: int, WCA_VISUAL_OWNER Data : constant 0x17, 90 | // Constant, Type: int, WCA_HOLOGRAPHIC Data : constant 0x18, 91 | // Constant, Type: int, WCA_EXCLUDED_FROM_DDA Data : constant 0x19, 92 | // Constant, Type: int, WCA_PASSIVEUPDATEMODE Data : constant 0x1A, 93 | // Constant, Type: int, WCA_LAST 94 | enum WINDOWCOMPOSITIONATTRIB : INT { // Determines what attribute is being 95 | // manipulated. 96 | WCA_ACCENT_POLICY = 97 | 0x13 // The attribute being get or set is an accent policy. 98 | }; 99 | 100 | // UserDefinedType: tagWINDOWCOMPOSITIONATTRIBDATA 101 | // Data : this+0x0, Member, Type: enum WINDOWCOMPOSITIONATTRIB, 102 | // Attrib Data : this+0x8, Member, Type: void *, pvData Data : 103 | // this+0x10, Member, Type: unsigned int, cbData 104 | struct WINDOWCOMPOSITIONATTRIBDATA { // Options for 105 | // [Get/Set]WindowCompositionAttribute. 106 | WINDOWCOMPOSITIONATTRIB Attrib; // Type of what is being get or set. 107 | LPVOID pvData; // Pointer to memory that will receive what is get or that 108 | // contains what will be set. 109 | UINT cbData; // Size of the data being pointed to by pvData. 110 | }; 111 | 112 | typedef BOOL(WINAPI *PFN_SET_WINDOW_COMPOSITION_ATTRIBUTE)( 113 | HWND, const WINDOWCOMPOSITIONATTRIBDATA *); 114 | 115 | static auto pSetWindowCompositionAttribute = 116 | (PFN_SET_WINDOW_COMPOSITION_ATTRIBUTE)GetProcAddress( 117 | GetModuleHandleW(L"user32.dll"), "SetWindowCompositionAttribute"); -------------------------------------------------------------------------------- /src/ui/ui.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include "GLFW/glfw3.h" 13 | #include "nanovg.h" 14 | 15 | #include "widget.h" 16 | 17 | namespace ui { 18 | struct render_target { 19 | std::shared_ptr root; 20 | GLFWwindow *window; 21 | 22 | // float: darkness of the acrylic effect, 0~1 23 | std::optional acrylic = {}; 24 | bool extend = false; 25 | bool transparent = false; 26 | bool no_focus = false; 27 | bool capture_all_input = false; 28 | bool decorated = true; 29 | bool topmost = false; 30 | bool resizable = false; 31 | bool vsync = true; 32 | std::string title = "Window"; 33 | NVGcontext *nvg = nullptr; 34 | int width = 1280; 35 | int height = 720; 36 | static std::atomic_int view_cnt; 37 | int view_id = view_cnt++; 38 | float dpi_scale = 1; 39 | float scroll_y = 0; 40 | int64_t last_rerender = 0; 41 | std::expected init(); 42 | 43 | static std::queue> main_thread_tasks; 44 | static std::mutex main_thread_tasks_mutex; 45 | static void post_main_thread_task(std::function task); 46 | 47 | static std::expected init_global(); 48 | void start_loop(); 49 | void render(); 50 | void resize(int width, int height); 51 | void set_position(int x, int y); 52 | void reset_view(); 53 | void close(); 54 | void hide(); 55 | void show(); 56 | void hide_as_close(); 57 | bool should_loop_stop_hide_as_close = false; 58 | std::optional> on_focus_changed; 59 | std::chrono::steady_clock clock{}; 60 | std::recursive_mutex rt_lock{}; 61 | std::mutex loop_thread_tasks_lock{}; 62 | std::queue> loop_thread_tasks{}; 63 | void post_loop_thread_task(std::function task); 64 | template 65 | T inline post_loop_thread_task(std::function task) { 66 | std::promise p; 67 | post_loop_thread_task([&]() { p.set_value(task()); }); 68 | return p.get_future().get(); 69 | } 70 | decltype(clock.now()) last_time = clock.now(); 71 | bool mouse_down = false, right_mouse_down = false; 72 | void *parent = nullptr; 73 | 74 | render_target() = default; 75 | ~render_target(); 76 | render_target operator=(const render_target &) = delete; 77 | render_target(const render_target &) = delete; 78 | }; 79 | } // namespace ui -------------------------------------------------------------------------------- /src/ui/widget.cc: -------------------------------------------------------------------------------- 1 | #include "widget.h" 2 | #include "ui.h" 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | void ui::widget::update_child_basic(update_context &ctx, 11 | std::shared_ptr &w) { 12 | if (!w) 13 | return; 14 | // handle dying time 15 | if (w->dying_time && w->dying_time.time <= 0) { 16 | w = nullptr; 17 | return; 18 | } 19 | w->parent = this; 20 | w->update(ctx); 21 | } 22 | 23 | void ui::widget::render_child_basic(nanovg_context ctx, 24 | std::shared_ptr &w) { 25 | if (!w) 26 | return; 27 | 28 | constexpr float big_number = 1e5; 29 | auto can_render_width = 30 | enable_child_clipping 31 | ? std::max(std::min(**w->width, **width - *w->x), 0.f) 32 | : big_number, 33 | can_render_height = 34 | enable_child_clipping 35 | ? std::max(std::min(**w->height, **height - *w->y), 0.f) 36 | : big_number; 37 | 38 | if (can_render_width > 0 && can_render_height > 0) { 39 | ctx.save(); 40 | w->render(ctx); 41 | ctx.restore(); 42 | } 43 | } 44 | 45 | void ui::widget::render(nanovg_context ctx) { 46 | if constexpr (false) 47 | if (_debug_offset_cache[0] != ctx.offset_x || 48 | _debug_offset_cache[1] != ctx.offset_y) { 49 | if (_debug_offset_cache[0] != -1 || _debug_offset_cache[1] != -1) { 50 | std::println( 51 | "[Warning] The offset during render is different from the " 52 | "offset during update: (update) {} {} vs (render) {} {} ({}, {})", 53 | _debug_offset_cache[0], _debug_offset_cache[1], ctx.offset_x, 54 | ctx.offset_y, (void *)this, typeid(*this).name()); 55 | } else { 56 | std::println( 57 | "[Warning] The update function is not called before render: {}", 58 | (void *)this); 59 | } 60 | } 61 | 62 | _debug_offset_cache[0] = -1; 63 | _debug_offset_cache[1] = -1; 64 | 65 | render_children(ctx.with_offset(*x, *y), children); 66 | } 67 | void ui::widget::update(update_context &ctx) { 68 | children_dirty = false; 69 | for (auto anim : anim_floats) { 70 | anim->update(ctx.delta_t); 71 | 72 | if (anim->updated()) { 73 | ctx.need_rerender = true; 74 | } 75 | } 76 | 77 | dying_time.update(ctx.delta_t); 78 | update_context upd = ctx.with_offset(*x, *y); 79 | update_children(upd, children); 80 | if constexpr (false) 81 | if (_debug_offset_cache[0] != -1 || _debug_offset_cache[1] != -1) { 82 | std::println( 83 | "[Warning] The update function is called twice with different " 84 | "offsets: {} {} vs {} {} ({})", 85 | _debug_offset_cache[0], _debug_offset_cache[1], ctx.offset_x, 86 | ctx.offset_y, (void *)this); 87 | } 88 | _debug_offset_cache[0] = ctx.offset_x; 89 | _debug_offset_cache[1] = ctx.offset_y; 90 | 91 | last_offset_x = ctx.offset_x; 92 | last_offset_y = ctx.offset_y; 93 | } 94 | void ui::widget::add_child(std::shared_ptr child) { 95 | children.push_back(std::move(child)); 96 | } 97 | 98 | bool ui::update_context::hovered(widget *w, bool hittest) const { 99 | auto hit = w->check_hit(*this); 100 | if (!hit) 101 | return false; 102 | 103 | if (hittest) { 104 | if (!hovered_widgets->empty()) { 105 | // iterate through parent chain 106 | auto p = w; 107 | while (p) { 108 | if (std::ranges::contains(*hovered_widgets, p)) { 109 | return true; 110 | } 111 | 112 | p = p->parent; 113 | } 114 | return false; 115 | } 116 | } 117 | 118 | return true; 119 | } 120 | float ui::widget::measure_height(update_context &ctx) { return height->dest(); } 121 | float ui::widget::measure_width(update_context &ctx) { return width->dest(); } 122 | void ui::widget_flex::update(update_context &ctx) { 123 | widget::update(ctx); 124 | auto forkctx = ctx.with_offset(*x, *y); 125 | reposition_children_flex(forkctx, children); 126 | } 127 | void ui::widget_flex::reposition_children_flex( 128 | update_context &ctx, std::vector> &children) { 129 | float x = 0, y = 0; 130 | float target_width = 0, target_height = 0; 131 | 132 | auto children_rev = 133 | reverse ? children | std::views::reverse | 134 | std::ranges::to>>() 135 | : children; 136 | 137 | for (auto &child : children_rev) { 138 | if (horizontal) { 139 | child->x->animate_to(x); 140 | x += child->measure_width(ctx) + gap; 141 | target_height = std::max(target_height, child->measure_height(ctx)); 142 | } else { 143 | child->y->animate_to(y); 144 | y += child->measure_height(ctx) + gap; 145 | target_width = std::max(target_width, child->measure_width(ctx)); 146 | } 147 | } 148 | 149 | if (!auto_size) { 150 | if (horizontal) { 151 | target_width = *width; 152 | } else { 153 | target_height = *height; 154 | } 155 | } 156 | 157 | if (horizontal) { 158 | width->animate_to(x - gap); 159 | height->animate_to(target_height); 160 | 161 | for (auto &child : children) { 162 | child->height->animate_to(target_height); 163 | } 164 | } else { 165 | width->animate_to(target_width); 166 | height->animate_to(y - gap); 167 | 168 | for (auto &child : children) { 169 | child->width->animate_to(target_width); 170 | } 171 | } 172 | } 173 | 174 | void ui::update_context::set_hit_hovered(widget *w) { 175 | hovered_widgets->push_back(w); 176 | } 177 | bool ui::update_context::mouse_clicked_on(widget *w, bool hittest) const { 178 | return mouse_clicked && hovered(w, hittest); 179 | } 180 | bool ui::update_context::mouse_down_on(widget *w, bool hittest) const { 181 | return mouse_down && hovered(w, hittest); 182 | } 183 | bool ui::update_context::mouse_clicked_on_hit(widget *w, bool hittest) { 184 | if (mouse_clicked_on(w, hittest)) { 185 | set_hit_hovered(w); 186 | return true; 187 | } 188 | return false; 189 | } 190 | bool ui::update_context::hovered_hit(widget *w, bool hittest) { 191 | if (hovered(w, hittest)) { 192 | set_hit_hovered(w); 193 | return true; 194 | } else { 195 | return false; 196 | } 197 | } 198 | bool ui::widget::check_hit(const update_context &ctx) { 199 | return ctx.mouse_x >= (x->dest() + ctx.offset_x) && 200 | ctx.mouse_x <= (x->dest() + width->dest() + ctx.offset_x) && 201 | ctx.mouse_y >= (y->dest() + ctx.offset_y) && 202 | ctx.mouse_y <= (y->dest() + height->dest() + ctx.offset_y); 203 | } 204 | void ui::widget::update_children( 205 | update_context &ctx, std::vector> &children) { 206 | for (auto &child : children) { 207 | update_child_basic(ctx, child); 208 | if (children_dirty) 209 | break; 210 | } 211 | 212 | // Remove dead children 213 | std::erase_if(children, [](auto &child) { return !child; }); 214 | } 215 | void ui::widget::render_children( 216 | nanovg_context ctx, std::vector> &children) { 217 | for (auto &child : children) { 218 | render_child_basic(ctx, child); 219 | } 220 | } 221 | void ui::text_widget::render(nanovg_context ctx) { 222 | widget::render(ctx); 223 | ctx.fontSize(font_size); 224 | ctx.fillColor(color.nvg()); 225 | ctx.textAlign(NVG_ALIGN_TOP | NVG_ALIGN_LEFT); 226 | ctx.fontFace(font_family.c_str()); 227 | 228 | ctx.text(*x, *y, text.c_str(), nullptr); 229 | } 230 | void ui::text_widget::update(update_context &ctx) { 231 | widget::update(ctx); 232 | ctx.vg.fontSize(font_size); 233 | ctx.vg.fontFace(font_family.c_str()); 234 | auto text = ctx.vg.measureText(this->text.c_str()); 235 | 236 | if (strink_horizontal) { 237 | width->animate_to(text.first); 238 | } 239 | 240 | if (strink_vertical) { 241 | height->animate_to(text.second); 242 | } 243 | } 244 | void ui::padding_widget::update(update_context &ctx) { 245 | auto off = ctx.with_offset(*padding_left, *padding_top); 246 | widget::update(off); 247 | 248 | float max_width = 0, max_height = 0; 249 | for (auto &child : children) { 250 | max_width = std::max(max_width, child->measure_width(ctx)); 251 | max_height = std::max(max_height, child->measure_height(ctx)); 252 | } 253 | 254 | width->animate_to(max_width + *padding_left + *padding_right); 255 | height->animate_to(max_height + *padding_top + *padding_bottom); 256 | } 257 | void ui::padding_widget::render(nanovg_context ctx) { 258 | ctx.transaction(); 259 | ctx.translate(**padding_left, **padding_top); 260 | widget::render(ctx); 261 | } 262 | ui::update_context ui::update_context::within(widget *w) const { 263 | auto copy = *this; 264 | copy.offset_x = w->last_offset_x; 265 | copy.offset_y = w->last_offset_y; 266 | return copy; 267 | } 268 | void ui::update_context::print_hover_info(widget *w) const { 269 | std::printf("widget(%p)\n\t hovered: %d x: %f y: %f width: %f height: %f \n\t" 270 | "mouse_x: %f mouse_y: %f (x: %d %d y: %d %d)\n", 271 | w, hovered(w), w->x->dest(), w->y->dest(), w->width->dest(), 272 | w->height->dest(), mouse_x, mouse_y, 273 | mouse_x >= (w->x->dest() + offset_x), 274 | mouse_x <= (w->x->dest() + w->width->dest() + offset_x), 275 | mouse_y >= (w->y->dest() + offset_y), 276 | mouse_y <= (w->y->dest() + w->height->dest() + offset_y)); 277 | } 278 | -------------------------------------------------------------------------------- /src/ui/widget.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "animator.h" 3 | #include "nanovg_wrapper.h" 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | namespace ui { 10 | struct render_target; 11 | struct widget; 12 | struct screen_info { 13 | int width, height; 14 | float dpi_scale; 15 | }; 16 | struct update_context { 17 | // time since last frame, in milliseconds 18 | float delta_t; 19 | // mouse position in window coordinates 20 | double mouse_x, mouse_y; 21 | bool mouse_down, right_mouse_down; 22 | // only true for one frame 23 | bool mouse_clicked, right_mouse_clicked; 24 | bool mouse_up; 25 | screen_info screen; 26 | float scroll_y; 27 | 28 | bool& need_rerender; 29 | 30 | // hit test, lifetime is not guaranteed 31 | std::shared_ptr> hovered_widgets = 32 | std::make_shared>(); 33 | void set_hit_hovered(widget *w); 34 | 35 | bool hovered(widget *w, bool hittest = true) const; 36 | void print_hover_info(widget *w) const; 37 | bool mouse_clicked_on(widget *w, bool hittest = true) const; 38 | bool mouse_down_on(widget *w, bool hittest = true) const; 39 | 40 | bool mouse_clicked_on_hit(widget *w, bool hittest = true); 41 | bool hovered_hit(widget *w, bool hittest = true); 42 | 43 | float offset_x = 0, offset_y = 0; 44 | render_target &rt; 45 | nanovg_context vg; 46 | 47 | update_context with_offset(float x, float y) const { 48 | auto copy = *this; 49 | copy.offset_x = x + offset_x; 50 | copy.offset_y = y + offset_y; 51 | return copy; 52 | } 53 | 54 | update_context with_reset_offset(float x = 0, float y = 0) const { 55 | auto copy = *this; 56 | copy.offset_x = x; 57 | copy.offset_y = y; 58 | return copy; 59 | } 60 | 61 | update_context within(widget *w) const; 62 | }; 63 | 64 | struct dying_time { 65 | float time = 100; 66 | bool _last_has_value = false; 67 | bool _changed = false; 68 | inline bool changed() const { 69 | return _last_has_value != has_value || _changed; 70 | } 71 | bool has_value = false; 72 | 73 | operator bool() const { return has_value; } 74 | inline float operator=(float t) { 75 | time = t; 76 | has_value = true; 77 | return t; 78 | } 79 | inline float operator-=(float t) { 80 | time -= t; 81 | has_value = true; 82 | return time; 83 | } 84 | inline void operator=(std::nullopt_t) { has_value = false; } 85 | inline void reset() { 86 | has_value = false; 87 | time = 0; 88 | } 89 | 90 | inline void update(float dt) { 91 | if (has_value && time > 0) { 92 | time -= dt; 93 | } 94 | 95 | if (_last_has_value != has_value) { 96 | _changed = true; 97 | _last_has_value = has_value; 98 | } else { 99 | _changed = false; 100 | } 101 | } 102 | }; 103 | 104 | /* 105 | All the widgets in the tree should be wrapped in a shared_ptr. 106 | If you want to use a widget in multiple places, you should create a new instance 107 | for each place. 108 | 109 | It is responsible for updating and rendering its children 110 | It also sets the offset for the children 111 | It's like `posision: relative` in CSS 112 | While all other widgets are like `position: absolute` 113 | */ 114 | struct widget : std::enable_shared_from_this { 115 | std::vector anim_floats{}; 116 | sp_anim_float anim_float(auto &&...args) { 117 | auto anim = 118 | std::make_shared(std::forward(args)...); 119 | anim_floats.push_back(anim); 120 | return anim; 121 | } 122 | 123 | sp_anim_float x = anim_float(), y = anim_float(), width = anim_float(), 124 | height = anim_float(); 125 | 126 | float _debug_offset_cache[2]; 127 | bool enable_child_clipping = false; 128 | 129 | float last_offset_x = 0, last_offset_y = 0; 130 | 131 | // Time until the widget is removed from the tree 132 | // in milliseconds 133 | // Widget itself will update this value 134 | // And its parent is responsible for removing it 135 | // when the time is up 136 | dying_time dying_time; 137 | 138 | widget* parent = nullptr; 139 | render_target *owner_rt = nullptr; 140 | 141 | virtual void render(nanovg_context ctx); 142 | virtual void update(update_context &ctx); 143 | virtual ~widget() = default; 144 | virtual float measure_height(update_context &ctx); 145 | virtual float measure_width(update_context &ctx); 146 | // Update children with the offset. 147 | // Also deal with the dying time. (If the widget is died, it will be set to 148 | // nullptr) 149 | void update_child_basic(update_context &ctx, std::shared_ptr &w); 150 | // Render children with the offset. 151 | void render_child_basic(nanovg_context ctx, std::shared_ptr &w); 152 | 153 | // Update children list in the widget manner 154 | // It will remove the dead children 155 | // It will also update the dying time 156 | // It will **NOT** update the children with the offset, call it with with_offset(*x, *y) if needed 157 | void update_children(update_context &ctx, 158 | std::vector> &children); 159 | // Render children list in the widget manner 160 | void render_children(nanovg_context ctx, 161 | std::vector> &children); 162 | 163 | template inline auto downcast() { 164 | return std::dynamic_pointer_cast(this->shared_from_this()); 165 | } 166 | 167 | virtual bool check_hit(const update_context &ctx); 168 | 169 | void add_child(std::shared_ptr child); 170 | std::vector> children; 171 | bool children_dirty = false; 172 | template 173 | inline std::shared_ptr emplace_child(Args &&...args) { 174 | auto child = std::make_shared(std::forward(args)...); 175 | children.emplace_back(child); 176 | return child; 177 | } 178 | 179 | template inline std::shared_ptr get_child() { 180 | for (auto &child : children) { 181 | if (auto c = child->downcast()) { 182 | return c; 183 | } 184 | } 185 | return nullptr; 186 | } 187 | 188 | template inline std::vector> get_children() { 189 | std::vector> res; 190 | for (auto &child : children) { 191 | if (auto c = child->downcast()) { 192 | res.push_back(c); 193 | } 194 | } 195 | return res; 196 | } 197 | }; 198 | 199 | // A widget with child which lays out children in a row or column 200 | struct widget_flex : public widget { 201 | float gap = 0; 202 | bool horizontal = false; 203 | bool auto_size = true; 204 | bool reverse = false; 205 | void reposition_children_flex(update_context &ctx, 206 | std::vector> &children); 207 | void update(update_context &ctx) override; 208 | }; 209 | 210 | // A widget that renders text 211 | struct text_widget : public widget { 212 | std::string text; 213 | float font_size = 14; 214 | std::string font_family = "main"; 215 | animated_color color = {this, 0, 0, 0, 1}; 216 | 217 | void render(nanovg_context ctx) override; 218 | 219 | bool strink_vertical = true, strink_horizontal = true; 220 | void update(update_context &ctx) override; 221 | }; 222 | 223 | // A widget that renders children in it with a padding 224 | struct padding_widget : public widget { 225 | sp_anim_float padding_left = anim_float(0), padding_right = anim_float(0), 226 | padding_top = anim_float(0), padding_bottom = anim_float(0); 227 | 228 | void update(update_context &ctx) override; 229 | void render(nanovg_context ctx) override; 230 | }; 231 | 232 | } // namespace ui -------------------------------------------------------------------------------- /src/ui_test/test.cc: -------------------------------------------------------------------------------- 1 | #include "GLFW/glfw3.h" 2 | #include "animator.h" 3 | #include "extra_widgets.h" 4 | #include "nanovg_wrapper.h" 5 | #include "ui.h" 6 | #include "widget.h" 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | struct test_widget : public ui::acrylic_background_widget { 16 | using super = ui::acrylic_background_widget; 17 | test_widget() : super() { 18 | x->animate_to(100); 19 | y->animate_to(100); 20 | width->animate_to(100); 21 | height->animate_to(100); 22 | 23 | acrylic_bg_color = nvgRGBAf(0, 0.5, 0.5, 0.5); 24 | update_color(); 25 | radius->animate_to(10); 26 | } 27 | 28 | ui::sp_anim_float color_transition = anim_float(256, 3000); 29 | void render(ui::nanovg_context ctx) override { 30 | super::render(ctx); 31 | ctx.fontFace("main"); 32 | ctx.fontSize(24); 33 | ctx.text(*x + 10, *y + 30, "Button", nullptr); 34 | } 35 | 36 | void update(ui::update_context &ctx) override { 37 | super::update(ctx); 38 | if (ctx.mouse_down_on(this)) { 39 | color_transition->animate_to(255); 40 | width->animate_to(200); 41 | height->animate_to(200); 42 | } else if (ctx.hovered(this)) { 43 | color_transition->animate_to(0); 44 | width->animate_to(150); 45 | height->animate_to(150); 46 | } else { 47 | color_transition->animate_to(128); 48 | width->animate_to(100); 49 | height->animate_to(100); 50 | } 51 | 52 | if (ctx.mouse_clicked_on(this)) { 53 | if (x->dest() == 100) { 54 | x->animate_to(200); 55 | y->animate_to(200); 56 | } else { 57 | x->animate_to(100); 58 | y->animate_to(100); 59 | } 60 | } 61 | } 62 | }; 63 | 64 | struct dying_widget_test : public ui::widget { 65 | using super = ui::widget; 66 | 67 | ui::sp_anim_float opacity = anim_float(0, 200); 68 | 69 | dying_widget_test() : super() { 70 | x->animate_to(100); 71 | y->animate_to(100); 72 | width->animate_to(100); 73 | height->animate_to(100); 74 | opacity->animate_to(255); 75 | } 76 | 77 | void render(ui::nanovg_context ctx) override { 78 | super::render(ctx); 79 | 80 | static std::string s = R"#()#"; 81 | static auto svg = nsvgParse(s.data(), 82 | "px", 96 83 | ); 84 | ctx.fillColor(nvgRGBAf(0.5, 0.5, 0, *opacity / 255.f)); 85 | ctx.fillRect(*x, *y, *width, *height); 86 | ctx.drawSVG(svg, *x, *y, *width, *height); 87 | // std::println("Rendering dying widget"); 88 | } 89 | 90 | 91 | void update(ui::update_context &ctx) override { 92 | super::update(ctx); 93 | if (ctx.mouse_down_on(this)) { 94 | dying_time = 200; 95 | } 96 | 97 | if (dying_time) { 98 | opacity->animate_to(0); 99 | } else if (ctx.hovered(this)) { 100 | opacity->animate_to(128); 101 | } else { 102 | opacity->animate_to(255); 103 | } 104 | } 105 | }; 106 | 107 | int main() { 108 | 109 | if (auto res = ui::render_target::init_global(); !res) { 110 | std::println("Failed to initialize global render target: {}", res.error()); 111 | return 1; 112 | } 113 | 114 | ui::render_target rt; 115 | rt.decorated = true; 116 | // rt.topmost = true; 117 | // rt.transparent = false; 118 | 119 | if (auto res = rt.init(); !res) { 120 | std::println("Failed to initialize render target: {}", res.error()); 121 | return 1; 122 | } 123 | nvgCreateFont(rt.nvg, "main", "C:\\WINDOWS\\FONTS\\msyh.ttc"); 124 | 125 | rt.start_loop(); 126 | return 0; 127 | 128 | glfwInit(); 129 | glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); 130 | glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); 131 | glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE); 132 | glfwWindowHint(GLFW_SCALE_TO_MONITOR, GLFW_TRUE); 133 | glfwWindowHint(GLFW_TRANSPARENT_FRAMEBUFFER, GLFW_TRUE); 134 | glfwWindowHint(GLFW_FLOATING, GLFW_TRUE); 135 | glfwWindowHint(GLFW_DECORATED, GLFW_FALSE); 136 | 137 | auto window = glfwCreateWindow(800, 600, "UI", nullptr, nullptr); 138 | glfwMakeContextCurrent(window); 139 | 140 | if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { 141 | std::cerr << "Failed to initialize GLAD" << std::endl; 142 | return -1; 143 | } 144 | 145 | while (!glfwWindowShouldClose(window)) { 146 | glClearColor(0, 0, 0, 0.3); 147 | glClear(GL_COLOR_BUFFER_BIT); 148 | glfwSwapBuffers(window); 149 | glfwPollEvents(); 150 | } 151 | 152 | return 0; 153 | } -------------------------------------------------------------------------------- /xmake.lua: -------------------------------------------------------------------------------- 1 | set_project("shell") 2 | set_policy("compatibility.version", "3.0") 3 | 4 | local version = "0.1.20" 5 | 6 | set_languages("c++2b") 7 | set_warnings("all") 8 | add_rules("plugin.compile_commands.autoupdate", {outputdir = "build"}) 9 | add_rules("mode.releasedbg") 10 | 11 | includes("dependencies/blook.lua") 12 | includes("dependencies/glfw.lua") 13 | includes("dependencies/reflect-cpp.lua") 14 | includes("dependencies/quickjs-ng.lua") 15 | 16 | set_runtimes("MT") 17 | add_requires("breeze-glfw", {alias = "glfw"}) 18 | add_requires("blook", "nanovg", "glad", "quickjs-ng", "nanosvg", "reflect-cpp") 19 | 20 | target("ui") 21 | set_kind("static") 22 | add_packages("glfw", "glad", "nanovg", "nanosvg", { 23 | public = true 24 | }) 25 | add_syslinks("dwmapi") 26 | add_files("src/ui/*.cc") 27 | add_headerfiles("src/ui/*.h") 28 | add_includedirs("src/ui", { 29 | public = true 30 | }) 31 | set_encodings("utf-8") 32 | 33 | target("ui_test") 34 | set_default(false) 35 | set_kind("binary") 36 | add_deps("ui") 37 | add_files("src/ui_test/*.cc") 38 | set_encodings("utf-8") 39 | add_tests("defualt") 40 | 41 | target("shell") 42 | set_kind("shared") 43 | add_packages("blook", "quickjs-ng", "reflect-cpp") 44 | add_deps("ui") 45 | add_syslinks("oleacc", "ole32", "oleaut32", "uuid", "comctl32", "comdlg32", "gdi32", "user32", "shell32", "kernel32", "advapi32", "psapi", "Winhttp", "dbghelp") 46 | add_rules("utils.bin2c", { 47 | extensions = {".js"} 48 | }) 49 | set_version(version) 50 | set_configdir("src/shell") 51 | add_configfiles("src/shell/build_info.h.in") 52 | on_config(function (package) 53 | local git_commit_hash = os.iorun("git rev-parse --short HEAD"):trim() 54 | local git_branch_name = os.iorun("git describe --all"):trim() 55 | local build_date_time = os.date("%Y-%m-%d %H:%M:%S") 56 | package:set("configvar", "GIT_COMMIT_HASH", git_commit_hash or "null") 57 | package:set("configvar", "GIT_BRANCH_NAME", git_branch_name or "null") 58 | package:set("configvar", "BUILD_DATE_TIME", build_date_time) 59 | end) 60 | add_files("src/shell/script/script.js") 61 | add_files("src/shell/**/*.cc", "src/shell/*.cc", "src/shell/**/*.c") 62 | set_encodings("utf-8") 63 | 64 | target("inject") 65 | set_kind("binary") 66 | add_syslinks("psapi", "user32", "shell32", "kernel32", "advapi32") 67 | add_files("src/inject/*.cc") 68 | add_deps("ui") 69 | set_basename("breeze") 70 | set_encodings("utf-8") 71 | add_rules("utils.bin2c", { 72 | extensions = {".png"} 73 | }) 74 | add_files("resources/icon-small.png") 75 | -- set_policy("windows.manifest.uac", "admin") 76 | add_ldflags("/subsystem:windows") 77 | 78 | --------------------------------------------------------------------------------