302 行
9.5 KiB
C++
302 行
9.5 KiB
C++
#include "wgfx.h"
|
|
|
|
#include <chrono>
|
|
#include <cwchar>
|
|
#include <random>
|
|
|
|
#pragma comment(lib, "gdiplus.lib")
|
|
|
|
using namespace Gdiplus;
|
|
|
|
namespace ui {
|
|
|
|
double g_scale = 1.0;
|
|
|
|
void InitDpi() {
|
|
// Per-monitor v2 when available; plain aware as fallback. Either way the
|
|
// process stops getting bitmap-stretched by DWM and we can scale layout
|
|
// metrics by the real DPI.
|
|
using SetCtxFn = BOOL(WINAPI*)(DPI_AWARENESS_CONTEXT);
|
|
HMODULE user32 = GetModuleHandleW(L"user32.dll");
|
|
if (auto fn = reinterpret_cast<SetCtxFn>(
|
|
reinterpret_cast<void*>(GetProcAddress(
|
|
user32, "SetProcessDpiAwarenessContext")))) {
|
|
if (!fn(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)) {
|
|
fn(DPI_AWARENESS_CONTEXT_SYSTEM_AWARE);
|
|
}
|
|
} else {
|
|
SetProcessDPIAware();
|
|
}
|
|
HDC dc = GetDC(nullptr);
|
|
g_scale = GetDeviceCaps(dc, LOGPIXELSX) / 96.0;
|
|
ReleaseDC(nullptr, dc);
|
|
}
|
|
|
|
double Now() {
|
|
using namespace std::chrono;
|
|
return duration<double>(steady_clock::now().time_since_epoch()).count();
|
|
}
|
|
|
|
double EaseLinear(double t) { return t; }
|
|
|
|
double EaseOutCubic(double t) {
|
|
double u = 1.0 - t;
|
|
return 1.0 - u * u * u;
|
|
}
|
|
|
|
double EaseInCubic(double t) { return t * t * t; }
|
|
|
|
double EaseInOutSine(double t) { return 0.5 - 0.5 * cos(t * 3.14159265358979); }
|
|
|
|
double EaseInOutQuad(double t) {
|
|
return t < 0.5 ? 2.0 * t * t : 1.0 - pow(-2.0 * t + 2.0, 2.0) / 2.0;
|
|
}
|
|
|
|
double EaseOutBack(double t) {
|
|
// Mirrors Qt's OutBack default overshoot (s = 1.70158).
|
|
const double s = 1.70158;
|
|
double u = t - 1.0;
|
|
return 1.0 + (s + 1.0) * u * u * u + s * u * u;
|
|
}
|
|
|
|
double Since(double start, double dur) {
|
|
if (dur <= 0.0) return 1.0;
|
|
double t = (Now() - start) / dur;
|
|
if (t < 0.0) t = 0.0;
|
|
if (t > 1.0) t = 1.0;
|
|
return t;
|
|
}
|
|
|
|
double Phase(double t, double t0, double t1, double (*ease)(double)) {
|
|
if (t <= t0) return 0.0;
|
|
if (t >= t1) return 1.0;
|
|
return ease((t - t0) / (t1 - t0));
|
|
}
|
|
|
|
Gdiplus::Color Rgba(int r, int g, int b, int a) {
|
|
return Gdiplus::Color(static_cast<BYTE>(a), static_cast<BYTE>(r),
|
|
static_cast<BYTE>(g), static_cast<BYTE>(b));
|
|
}
|
|
|
|
Gdiplus::Color Hex(unsigned rgb, int a) {
|
|
return Rgba((rgb >> 16) & 0xff, (rgb >> 8) & 0xff, rgb & 0xff, a);
|
|
}
|
|
|
|
Gdiplus::GraphicsPath* RoundedRectPath(const Gdiplus::RectF& r, float radius) {
|
|
auto* p = new Gdiplus::GraphicsPath();
|
|
float rad = radius;
|
|
float m = r.Width < r.Height ? r.Width : r.Height;
|
|
m /= 2.0f;
|
|
if (rad > m) rad = m;
|
|
if (rad <= 0.0f) {
|
|
p->AddRectangle(r);
|
|
return p;
|
|
}
|
|
float d = rad * 2.0f;
|
|
p->AddArc(r.X, r.Y, d, d, 180.0f, 90.0f);
|
|
p->AddArc(r.X + r.Width - d, r.Y, d, d, 270.0f, 90.0f);
|
|
p->AddArc(r.X + r.Width - d, r.Y + r.Height - d, d, d, 0.0f, 90.0f);
|
|
p->AddArc(r.X, r.Y + r.Height - d, d, d, 90.0f, 90.0f);
|
|
p->CloseFigure();
|
|
return p;
|
|
}
|
|
|
|
namespace {
|
|
|
|
std::map<std::wstring, Gdiplus::Font*>& FontCache() {
|
|
static std::map<std::wstring, Gdiplus::Font*> cache;
|
|
return cache;
|
|
}
|
|
|
|
const Gdiplus::FontFamily& Family(const wchar_t* name) {
|
|
static std::map<std::wstring, Gdiplus::FontFamily*> fams;
|
|
auto it = fams.find(name);
|
|
if (it != fams.end()) return *it->second;
|
|
auto* f = new Gdiplus::FontFamily(name);
|
|
if (f->GetLastStatus() != Gdiplus::Ok) {
|
|
delete f;
|
|
f = new Gdiplus::FontFamily(L"Segoe UI"); // fallback
|
|
}
|
|
fams[name] = f;
|
|
return *f;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
Gdiplus::StringFormat& NearFormat() {
|
|
static StringFormat sf(StringFormat::GenericDefault());
|
|
return sf;
|
|
}
|
|
|
|
Gdiplus::Font* Font(const wchar_t* family, float px, bool bold, bool italic) {
|
|
int style = bold ? FontStyleBold : FontStyleRegular;
|
|
if (italic) style |= FontStyleItalic;
|
|
wchar_t key[128];
|
|
_snwprintf_s(key, _TRUNCATE, L"%s|%.1f|%d", family, px, style);
|
|
auto& cache = FontCache();
|
|
auto it = cache.find(key);
|
|
if (it != cache.end()) return it->second;
|
|
auto* f = new Gdiplus::Font(&Family(family), px, style, UnitPixel);
|
|
cache[key] = f;
|
|
return f;
|
|
}
|
|
|
|
float LineHeight(Gdiplus::Graphics& g, Gdiplus::Font* f) {
|
|
return f->GetHeight(&g);
|
|
}
|
|
|
|
float TextWidth(Gdiplus::Graphics& g, Gdiplus::Font* f, const std::wstring& s) {
|
|
Gdiplus::RectF bounds;
|
|
g.MeasureString(s.c_str(), static_cast<INT>(s.size()), f,
|
|
Gdiplus::RectF(0, 0, 10000, 10000), &NearFormat(), &bounds);
|
|
return bounds.Width;
|
|
}
|
|
|
|
void DrawText(Gdiplus::Graphics& g, Gdiplus::Font* f, const std::wstring& s,
|
|
float x, float y, const Gdiplus::Color& c, float wrapWidth) {
|
|
Gdiplus::SolidBrush brush(c);
|
|
Gdiplus::RectF layout(x, y, wrapWidth, LineHeight(g, f) * 3.0f + 4.0f);
|
|
g.DrawString(s.c_str(), static_cast<INT>(s.size()), f, layout,
|
|
&NearFormat(), &brush);
|
|
}
|
|
|
|
void DrawTextCentered(Gdiplus::Graphics& g, Gdiplus::Font* f,
|
|
const std::wstring& s, float centerX, float y,
|
|
const Gdiplus::Color& c, float wrapWidth) {
|
|
Gdiplus::SolidBrush brush(c);
|
|
Gdiplus::RectF layout(centerX - wrapWidth / 2.0f, y, wrapWidth,
|
|
LineHeight(g, f) * 3.0f + 4.0f);
|
|
StringFormat sf(&NearFormat());
|
|
sf.SetAlignment(StringAlignmentCenter);
|
|
g.DrawString(s.c_str(), static_cast<INT>(s.size()), f, layout, &sf,
|
|
&brush);
|
|
}
|
|
|
|
std::wstring ElideMiddle(Gdiplus::Graphics& g, Gdiplus::Font* f,
|
|
const std::wstring& s, float maxW) {
|
|
if (TextWidth(g, f, s) <= maxW || s.size() < 3) return s;
|
|
size_t head = s.size() / 2, tail = s.size() / 2;
|
|
// Shrink the middle one char at a time until it fits, then insert an
|
|
// ellipsis in the gap.
|
|
while (head > 0 && tail < s.size()) {
|
|
std::wstring cut = s.substr(0, head) + L'\u2026' + s.substr(tail);
|
|
if (TextWidth(g, f, cut) <= maxW) return cut;
|
|
// Trim alternately from the middle outwards.
|
|
if (head > tail - s.size() / 2) --head; else ++tail;
|
|
}
|
|
return s.substr(0, head) + L'\u2026';
|
|
}
|
|
|
|
// ----- LayeredCanvas -----
|
|
|
|
LayeredCanvas::~LayeredCanvas() { Free(); }
|
|
|
|
void LayeredCanvas::Free() {
|
|
delete gfx_;
|
|
delete wrap_;
|
|
if (bmp_) DeleteObject(bmp_);
|
|
if (dc_) DeleteDC(dc_);
|
|
gfx_ = nullptr;
|
|
wrap_ = nullptr;
|
|
bmp_ = nullptr;
|
|
dc_ = nullptr;
|
|
bits_ = nullptr;
|
|
w_ = h_ = 0;
|
|
}
|
|
|
|
bool LayeredCanvas::Resize(int w, int h) {
|
|
if (w < 1 || h < 1) return false;
|
|
if (w == w_ && h == h_) return true;
|
|
Free();
|
|
|
|
BITMAPINFO bi{};
|
|
bi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
|
|
bi.bmiHeader.biWidth = w;
|
|
bi.bmiHeader.biHeight = -h; // top-down
|
|
bi.bmiHeader.biPlanes = 1;
|
|
bi.bmiHeader.biBitCount = 32;
|
|
bi.bmiHeader.biCompression = BI_RGB;
|
|
|
|
dc_ = CreateCompatibleDC(nullptr);
|
|
bmp_ = CreateDIBSection(nullptr, &bi, DIB_RGB_COLORS, &bits_, nullptr, 0);
|
|
if (!dc_ || !bmp_ || !bits_) {
|
|
Free();
|
|
return false;
|
|
}
|
|
SelectObject(dc_, bmp_);
|
|
|
|
// Wrap the DIB memory as a premultiplied-ARGB GDI+ bitmap: everything
|
|
// drawn through gfx_ lands directly in the DIB, already in the exact
|
|
// format UpdateLayeredWindow expects.
|
|
wrap_ = new Gdiplus::Bitmap(w, h, w * 4, PixelFormat32bppPARGB,
|
|
static_cast<BYTE*>(bits_));
|
|
gfx_ = new Gdiplus::Graphics(wrap_);
|
|
if (gfx_->GetLastStatus() != Gdiplus::Ok) {
|
|
Free();
|
|
return false;
|
|
}
|
|
gfx_->SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias);
|
|
gfx_->SetTextRenderingHint(Gdiplus::TextRenderingHintAntiAlias);
|
|
gfx_->SetInterpolationMode(Gdiplus::InterpolationModeHighQualityBicubic);
|
|
w_ = w;
|
|
h_ = h;
|
|
return true;
|
|
}
|
|
|
|
void LayeredCanvas::Clear() {
|
|
if (gfx_) gfx_->Clear(Gdiplus::Color(0, 0, 0, 0));
|
|
}
|
|
|
|
Gdiplus::Graphics& LayeredCanvas::g() { return *gfx_; }
|
|
|
|
bool LayeredCanvas::Present(HWND hwnd, BYTE constAlpha) {
|
|
if (!dc_ || !hwnd) return false;
|
|
HDC screen = GetDC(nullptr);
|
|
POINT src{0, 0};
|
|
SIZE sz{w_, h_};
|
|
BLENDFUNCTION bf{AC_SRC_OVER, 0, constAlpha, AC_SRC_ALPHA};
|
|
BOOL ok = UpdateLayeredWindow(hwnd, screen, nullptr, &sz, dc_, &src, 0,
|
|
&bf, ULW_ALPHA);
|
|
ReleaseDC(nullptr, screen);
|
|
return ok != FALSE;
|
|
}
|
|
|
|
std::wstring RandomIdent(int minLen, int maxLen) {
|
|
static const wchar_t kAlphabet[] =
|
|
L"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
|
const int kLetters = 52;
|
|
const int kAll = static_cast<int>(wcslen(kAlphabet));
|
|
std::mt19937 rng(static_cast<unsigned>(
|
|
std::random_device{}() ^
|
|
(std::hash<double>{}(Now()) << 1)));
|
|
int len = minLen + static_cast<int>(rng() % (maxLen - minLen + 1));
|
|
std::wstring s;
|
|
s.reserve(len);
|
|
s += kAlphabet[rng() % kLetters];
|
|
for (int i = 1; i < len; ++i) s += kAlphabet[rng() % kAll];
|
|
return s;
|
|
}
|
|
|
|
void LogLine(const std::wstring& msg) {
|
|
wchar_t temp[MAX_PATH]{};
|
|
GetTempPathW(MAX_PATH, temp);
|
|
std::wstring path = std::wstring(temp) + L"openzen-loader.log";
|
|
HANDLE f = CreateFileW(path.c_str(), FILE_APPEND_DATA, FILE_SHARE_READ,
|
|
nullptr, OPEN_ALWAYS,
|
|
FILE_ATTRIBUTE_NORMAL, nullptr);
|
|
if (f == INVALID_HANDLE_VALUE) return;
|
|
SYSTEMTIME st{};
|
|
GetLocalTime(&st);
|
|
wchar_t line[1024];
|
|
_snwprintf_s(line, _TRUNCATE,
|
|
L"[%02u:%02u:%02u.%03u] %s\r\n",
|
|
st.wHour, st.wMinute, st.wSecond, st.wMilliseconds,
|
|
msg.c_str());
|
|
DWORD written = 0;
|
|
WriteFile(f, line, static_cast<DWORD>(wcslen(line) * sizeof(wchar_t)),
|
|
&written, nullptr);
|
|
CloseHandle(f);
|
|
}
|
|
|
|
} // namespace ui
|