mirror of
https://github.com/Blinue/Magpie.git
synced 2026-06-24 02:04:10 +00:00
421 lines
12 KiB
C++
421 lines
12 KiB
C++
#include "pch.h"
|
|
#include "ImGuiImpl.h"
|
|
#include "CursorManager.h"
|
|
#include "DeviceResources.h"
|
|
#include "ImGuiBackend.h"
|
|
#include "Logger.h"
|
|
#include "Renderer.h"
|
|
#include "ScalingWindow.h"
|
|
#include "StrHelper.h"
|
|
#include "Win32Helper.h"
|
|
#include <imgui.h>
|
|
#include <imgui_internal.h>
|
|
#include <ranges>
|
|
|
|
namespace Magpie {
|
|
|
|
static bool operator==(const ImVec2& l, const ImVec2& r) noexcept {
|
|
return l.x == r.x && l.y == r.y;
|
|
}
|
|
|
|
static bool operator==(const ImVec4& l, const ImVec4& r) noexcept {
|
|
return l.x == r.x && l.y == r.y && l.z == r.z && l.w == r.w;
|
|
}
|
|
|
|
static const char* GetWindowIDFromName(const char* name) noexcept {
|
|
size_t idPos = std::string_view(name).find("##");
|
|
if (idPos == std::string_view::npos) {
|
|
return name;
|
|
} else {
|
|
return name + idPos + 2;
|
|
}
|
|
}
|
|
|
|
ImGuiImpl::~ImGuiImpl() noexcept {
|
|
if (ImGui::GetCurrentContext()) {
|
|
ImGui::DestroyContext();
|
|
}
|
|
}
|
|
|
|
bool ImGuiImpl::Initialize(DeviceResources& deviceResources) noexcept {
|
|
#ifdef _DEBUG
|
|
// 检查 ImGUI 版本是否匹配
|
|
if (!IMGUI_CHECKVERSION()) {
|
|
Logger::Get().Error("ImGui 的头文件与链接库版本不同");
|
|
return false;
|
|
}
|
|
#endif
|
|
|
|
ImGui::CreateContext();
|
|
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
io.BackendPlatformName = "Magpie";
|
|
io.ConfigFlags |= ImGuiConfigFlags_NavNoCaptureKeyboard | ImGuiConfigFlags_NoMouseCursorChange;
|
|
// 禁用 ini 配置文件
|
|
io.IniFilename = nullptr;
|
|
#ifndef _DEBUG
|
|
// Release 配置下禁用重复 ID 检查
|
|
io.ConfigDebugHighlightIdConflicts = false;
|
|
#endif
|
|
|
|
if (!_backend.Initialize(deviceResources)) {
|
|
Logger::Get().Error("初始化 ImGuiBackend 失败");
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool ImGuiImpl::BuildFonts() noexcept {
|
|
return _backend.BuildFonts();
|
|
}
|
|
|
|
void ImGuiImpl::NewFrame(
|
|
phmap::flat_hash_map<std::string, OverlayWindowOption>& windowOptions,
|
|
float fittsLawAdjustment,
|
|
float dpiScale
|
|
) noexcept {
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
|
|
{
|
|
const SIZE destSize = Win32Helper::GetSizeOfRect(ScalingWindow::Get().Renderer().DestRect());
|
|
ImVec2 newDisplaySize((float)destSize.cx, (float)destSize.cy);
|
|
if (io.DisplaySize != newDisplaySize) {
|
|
io.DisplaySize = newDisplaySize;
|
|
// 调整缩放窗口尺寸时强制调整叠加层窗口位置
|
|
_windowRects.clear();
|
|
}
|
|
}
|
|
|
|
_UpdateMousePos(fittsLawAdjustment);
|
|
|
|
// 不接受键盘输入
|
|
if (io.WantCaptureKeyboard) {
|
|
io.AddKeyEvent(ImGuiKey_Enter, true);
|
|
io.AddKeyEvent(ImGuiKey_Enter, false);
|
|
}
|
|
|
|
ImGui::NewFrame();
|
|
|
|
for (ImGuiWindow* window : ImGui::GetCurrentContext()->Windows) {
|
|
if (window->Flags & (ImGuiWindowFlags_Tooltip | ImGuiWindowFlags_NoMove)) {
|
|
continue;
|
|
}
|
|
|
|
// 排除 Debug##Default 窗口和尚未初始化完成的窗口
|
|
if (window->IsFallbackWindow || window->Appearing) {
|
|
continue;
|
|
}
|
|
|
|
ImVec2 pos = window->Pos;
|
|
|
|
// 将窗口限制在视口内
|
|
if (io.DisplaySize.x > window->Size.x) {
|
|
pos.x = std::clamp(pos.x, 0.0f, io.DisplaySize.x - window->Size.x);
|
|
} else {
|
|
pos.x = 0;
|
|
}
|
|
|
|
if (io.DisplaySize.y > window->Size.y) {
|
|
pos.y = std::clamp(pos.y, 0.0f, io.DisplaySize.y - window->Size.y);
|
|
} else {
|
|
pos.y = 0;
|
|
}
|
|
|
|
const char* windowId = GetWindowIDFromName(window->Name);
|
|
if (auto it = windowOptions.find(windowId); it != windowOptions.end()) {
|
|
OverlayWindowOption& option = it->second;
|
|
|
|
auto it1 = _windowRects.find(windowId);
|
|
if (it1 == _windowRects.end()) {
|
|
// 第一次显示或调整缩放窗口大小时叠加层窗口应根据规则调整位置
|
|
|
|
if (option.hArea == 0) {
|
|
pos.x = option.hPos * dpiScale;
|
|
} else if (option.hArea == 1) {
|
|
pos.x = io.DisplaySize.x * option.hPos - window->Size.x / 2;
|
|
} else if (option.hArea == 2) {
|
|
pos.x = io.DisplaySize.x - option.hPos * dpiScale - window->Size.x;
|
|
} else {
|
|
assert(false);
|
|
}
|
|
|
|
if (option.vArea == 0) {
|
|
pos.y = option.vPos * dpiScale;
|
|
} else if (option.vArea == 1) {
|
|
pos.y = io.DisplaySize.y * option.vPos - window->Size.y / 2;
|
|
} else if (option.vArea == 2) {
|
|
pos.y = io.DisplaySize.y - option.vPos * dpiScale - window->Size.y;
|
|
} else {
|
|
assert(false);
|
|
}
|
|
|
|
// 再次将窗口限制在视口内
|
|
if (io.DisplaySize.x > window->Size.x) {
|
|
pos.x = std::clamp(pos.x, 0.0f, io.DisplaySize.x - window->Size.x);
|
|
} else {
|
|
pos.x = 0;
|
|
}
|
|
|
|
if (io.DisplaySize.y > window->Size.y) {
|
|
pos.y = std::clamp(pos.y, 0.0f, io.DisplaySize.y - window->Size.y);
|
|
} else {
|
|
pos.y = 0;
|
|
}
|
|
} else if (it1->second != ImVec4(pos.x, pos.y, window->Size.x, window->Size.y)) {
|
|
// 当且仅当用户移动窗口或调整窗口大小后后重新计算贴靠的边,调整缩放窗口大小时应保持
|
|
// 贴靠的边不变。我们根据两侧边距的比例决定贴靠哪边或者都不贴靠。
|
|
|
|
// 这些阈值决定是否贴靠在某一边上,它们不是定值,而是窗口尺寸和画面尺寸的比例。这个
|
|
// 算法的效果出乎意料的好,因为窗口两侧边距较大时人对比例更敏感,较小时则对差值更敏
|
|
// 感。
|
|
const float thresholdX = std::max(window->Size.x / io.DisplaySize.x, 0.2f);
|
|
const float thresholdY = std::max(window->Size.y / io.DisplaySize.y, 0.2f);
|
|
|
|
// 根据左右边距比例决定贴靠
|
|
float ratio = pos.x / (io.DisplaySize.x - pos.x - window->Size.x);
|
|
if (ratio < thresholdX) {
|
|
option.hArea = 0;
|
|
option.hPos = pos.x / dpiScale;
|
|
} else if (ratio <= 1 / thresholdX) {
|
|
option.hArea = 1;
|
|
option.hPos = (pos.x + window->Size.x / 2) / io.DisplaySize.x;
|
|
} else {
|
|
option.hArea = 2;
|
|
option.hPos = (io.DisplaySize.x - pos.x - window->Size.x) / dpiScale;
|
|
}
|
|
|
|
// 根据上下边距比例决定贴靠
|
|
ratio = pos.y / (io.DisplaySize.y - pos.y - window->Size.y);
|
|
if (ratio < thresholdY) {
|
|
option.vArea = 0;
|
|
option.vPos = pos.y / dpiScale;
|
|
} else if (ratio <= 1 / thresholdY) {
|
|
option.vArea = 1;
|
|
option.vPos = (pos.y + window->Size.y / 2) / io.DisplaySize.y;
|
|
} else {
|
|
option.vArea = 2;
|
|
option.vPos = (io.DisplaySize.y - pos.y - window->Size.y) / dpiScale;
|
|
}
|
|
}
|
|
|
|
ImGui::SetWindowPos(window, pos);
|
|
|
|
// 此时 window->Pos 已更新,记录新的窗口位置
|
|
_windowRects[windowId] = ImVec4(window->Pos.x, window->Pos.y, window->Size.x, window->Size.y);
|
|
} else {
|
|
ImGui::SetWindowPos(window, pos);
|
|
}
|
|
}
|
|
|
|
// 调整缩放窗口大小或鼠标被前台窗口捕获时避免鼠标跳跃
|
|
CursorManager& cursorManager = ScalingWindow::Get().CursorManager();
|
|
if (!ScalingWindow::Get().IsResizingOrMoving() && !cursorManager.IsCursorCapturedOnForeground()) {
|
|
cursorManager.IsCursorOnOverlay(io.WantCaptureMouse);
|
|
}
|
|
}
|
|
|
|
void ImGuiImpl::Draw(POINT drawOffset) noexcept {
|
|
ImGui::Render();
|
|
|
|
const RECT& rendererRect = ScalingWindow::Get().RendererRect();
|
|
const RECT& destRect = ScalingWindow::Get().Renderer().DestRect();
|
|
const POINT viewportOffset = {
|
|
destRect.left - rendererRect.left + drawOffset.x,
|
|
destRect.top - rendererRect.top + drawOffset.y
|
|
};
|
|
_backend.RenderDrawData(*ImGui::GetDrawData(), viewportOffset);
|
|
}
|
|
|
|
void ImGuiImpl::Tooltip(
|
|
const char* content,
|
|
float dpiScale,
|
|
const char* description,
|
|
float maxWidth
|
|
) noexcept {
|
|
static constexpr float DESCRIPTION_SCALE = 0.9f;
|
|
|
|
ImVec2 padding = ImGui::GetStyle().WindowPadding;
|
|
ImVec2 contentSize = ImGui::CalcTextSize(content, nullptr, false, maxWidth - 2 * padding.x);
|
|
ImVec2 descriptionSize{};
|
|
if (description) {
|
|
float oldFontScale = ImGui::GetIO().FontGlobalScale;
|
|
ImGui::GetIO().FontGlobalScale *= DESCRIPTION_SCALE;
|
|
ImGui::PushFont(ImGui::GetFont());
|
|
descriptionSize = ImGui::CalcTextSize(description, nullptr, false, maxWidth - 2 * padding.x);
|
|
ImGui::GetIO().FontGlobalScale = oldFontScale;
|
|
ImGui::PopFont();
|
|
}
|
|
// 稍微增加高度,否则下边框比上边框稍窄
|
|
ImVec2 windowSize(
|
|
std::max(contentSize.x, descriptionSize.x) + 2 * padding.x,
|
|
contentSize.y + descriptionSize.y + 2 * padding.y + 1.5f * dpiScale
|
|
);
|
|
ImGui::SetNextWindowSize(windowSize);
|
|
|
|
ImVec2 windowPos = ImGui::GetMousePos();
|
|
windowPos.x += 16.0f * dpiScale * ImGui::GetStyle().MouseCursorScale;
|
|
windowPos.y += 8.0f * dpiScale * ImGui::GetStyle().MouseCursorScale;
|
|
|
|
SIZE outputSize = Win32Helper::GetSizeOfRect(ScalingWindow::Get().Renderer().DestRect());
|
|
windowPos.x = std::clamp(windowPos.x, 0.0f, outputSize.cx - windowSize.x);
|
|
windowPos.y = std::clamp(windowPos.y, 0.0f, outputSize.cy - windowSize.y);
|
|
|
|
ImGui::SetNextWindowPos(windowPos);
|
|
|
|
ImGui::SetNextWindowBgAlpha(ImGui::GetStyle().Colors[ImGuiCol_PopupBg].w);
|
|
ImGui::Begin("tooltip", NULL,
|
|
ImGuiWindowFlags_NoInputs |
|
|
ImGuiWindowFlags_NoDecoration |
|
|
ImGuiWindowFlags_NoMove |
|
|
ImGuiWindowFlags_NoSavedSettings |
|
|
ImGuiWindowFlags_AlwaysAutoResize |
|
|
ImGuiWindowFlags_NoFocusOnAppearing);
|
|
|
|
ImGui::PushTextWrapPos(maxWidth - padding.x);
|
|
ImGui::TextUnformatted(content);
|
|
if (description) {
|
|
ImGui::PushStyleColor(ImGuiCol_Text, { 1.0f,1.0f,1.0f,0.8f });
|
|
float oldFontScale = ImGui::GetIO().FontGlobalScale;
|
|
ImGui::GetIO().FontGlobalScale *= DESCRIPTION_SCALE;
|
|
ImGui::PushFont(ImGui::GetFont());
|
|
ImGui::TextUnformatted(description);
|
|
ImGui::GetIO().FontGlobalScale = oldFontScale;
|
|
ImGui::PopFont();
|
|
ImGui::PopStyleColor();
|
|
}
|
|
ImGui::PopTextWrapPos();
|
|
|
|
ImGui::BringWindowToDisplayFront(ImGui::GetCurrentWindow());
|
|
ImGui::End();
|
|
}
|
|
|
|
void ImGuiImpl::_UpdateMousePos(float fittsLawAdjustment) noexcept {
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
io.MousePos = ImVec2(-FLT_MAX, -FLT_MAX);
|
|
|
|
// 调整缩放窗口大小或鼠标被前台窗口捕获时不应和叠加层交互
|
|
const CursorManager& cursorManager = ScalingWindow::Get().CursorManager();
|
|
if (ScalingWindow::Get().IsResizingOrMoving() || cursorManager.IsCursorCapturedOnForeground()) {
|
|
return;
|
|
}
|
|
|
|
const POINT cursorPos = cursorManager.CursorPos();
|
|
|
|
// 转换为目标矩形局部坐标
|
|
const RECT& destRect = ScalingWindow::Get().Renderer().DestRect();
|
|
io.MousePos.x = float(cursorPos.x - destRect.left);
|
|
io.MousePos.y = float(cursorPos.y - destRect.top);
|
|
|
|
// 下移鼠标的逻辑位置使得在上边缘可以选中工具栏按钮
|
|
if (io.MousePos.y >= 0 && io.MousePos.y < fittsLawAdjustment) {
|
|
io.MousePos.y = fittsLawAdjustment;
|
|
}
|
|
}
|
|
|
|
void ImGuiImpl::ClearStates() noexcept {
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
io.MousePos = ImVec2(-FLT_MAX, -FLT_MAX);
|
|
std::fill(std::begin(io.MouseDown), std::end(io.MouseDown), false);
|
|
|
|
CursorManager& cursorManager = ScalingWindow::Get().CursorManager();
|
|
cursorManager.IsCursorCapturedOnOverlay(false);
|
|
cursorManager.IsCursorOnOverlay(false);
|
|
|
|
// 更新状态
|
|
ImGui::NewFrame();
|
|
ImGui::EndFrame();
|
|
|
|
if (io.WantCaptureMouse) {
|
|
// 拖拽时隐藏 UI 需渲染两帧才能重置 WantCaptureMouse
|
|
ImGui::NewFrame();
|
|
ImGui::EndFrame();
|
|
}
|
|
}
|
|
|
|
void ImGuiImpl::MessageHandler(UINT msg, WPARAM wParam, LPARAM /*lParam*/) noexcept {
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
|
|
if (!io.WantCaptureMouse) {
|
|
return;
|
|
}
|
|
|
|
// 缩放窗口不会收到双击消息
|
|
switch (msg) {
|
|
case WM_LBUTTONDOWN:
|
|
case WM_RBUTTONDOWN:
|
|
{
|
|
if (!ImGui::IsAnyMouseDown()) {
|
|
ScalingWindow::Get().CursorManager().IsCursorCapturedOnOverlay(true);
|
|
}
|
|
|
|
io.MouseDown[msg == WM_LBUTTONDOWN ? 0 : 1] = true;
|
|
break;
|
|
}
|
|
case WM_LBUTTONUP:
|
|
case WM_RBUTTONUP:
|
|
{
|
|
io.MouseDown[msg == WM_LBUTTONUP ? 0 : 1] = false;
|
|
|
|
if (!ImGui::IsAnyMouseDown()) {
|
|
ScalingWindow::Get().CursorManager().IsCursorCapturedOnOverlay(false);
|
|
}
|
|
|
|
break;
|
|
}
|
|
case WM_MOUSEWHEEL:
|
|
{
|
|
io.MouseWheel += (float)GET_WHEEL_DELTA_WPARAM(wParam) / (float)WHEEL_DELTA;
|
|
break;
|
|
}
|
|
case WM_MOUSEHWHEEL:
|
|
{
|
|
io.MouseWheelH += (float)GET_WHEEL_DELTA_WPARAM(wParam) / (float)WHEEL_DELTA;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
std::optional<ImVec4> ImGuiImpl::GetWindowRect(const char* id) const noexcept {
|
|
const std::string suffix = StrHelper::Concat("##", id);
|
|
for (ImGuiWindow* window : ImGui::GetCurrentContext()->Windows) {
|
|
if (std::string_view(window->Name).ends_with(suffix)) {
|
|
return ImVec4(
|
|
window->Pos.x,
|
|
window->Pos.y,
|
|
window->Pos.x + window->Size.x,
|
|
window->Pos.y + window->Size.y
|
|
);
|
|
}
|
|
}
|
|
|
|
return std::nullopt;
|
|
}
|
|
|
|
const char* ImGuiImpl::GetHoveredWindowId() const noexcept {
|
|
const ImVec2 mousePos = ImGui::GetIO().MousePos;
|
|
// 自顶向下遍历
|
|
for (ImGuiWindow* window : ImGui::GetCurrentContext()->Windows | std::views::reverse) {
|
|
// 排除不接受鼠标输入的窗口,来自
|
|
// https://github.com/ocornut/imgui/blob/77f1d3b317c400c34ee02fe9a5354d0d757b55ca/imgui.cpp#L5855
|
|
if (!window->WasActive || window->Hidden) {
|
|
continue;
|
|
}
|
|
if (window->Flags & ImGuiWindowFlags_NoMouseInputs) {
|
|
continue;
|
|
}
|
|
|
|
if (window->Rect().Contains(mousePos)) {
|
|
return GetWindowIDFromName(window->Name);
|
|
}
|
|
|
|
// 弹窗会阻止和其他窗口交互
|
|
if (window->Flags & ImGuiWindowFlags_Popup) {
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
}
|