-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathwindow.cpp
More file actions
379 lines (314 loc) · 13.7 KB
/
Copy pathwindow.cpp
File metadata and controls
379 lines (314 loc) · 13.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
#ifdef _WIN32
#ifndef UNICODE
#define UNICODE
#endif
#include "window.hpp"
#include "parameters.hpp"
#include <windows.h>
#include <tchar.h>
#include <iostream>
// Include GDI+ for high quality antialiasing
#include <gdiplus.h>
using namespace Parameters;
/*
Windows window management with Win32 is a bit more verbose. To achieve the same visual effect as I can with AppKit, I opted to work with GDI+, a Microsoft-recommended addon to the default GDI which allows for easy anti-aliasing and better object handling for objects I want to render.
Another limitation of the Windows implementation (arising from it's verbosity) compared to Mac is that AppKit uses Apple's CoreText, considered one of the highest quality text renderers in the world. The closest I can get with GDI+ is the ClearType antialiasing which does some RGB averaging between pixels. As a result, the text is always going to look worse on Windows.
*/
// GDI+ global initialisations
ULONG_PTR gdiplusToken;
static Gdiplus::PrivateFontCollection *g_fontCollection = nullptr;
static Gdiplus::FontFamily *g_customFontFamily = nullptr;
/*
Struct to hold win-specific rendering context.
We want to try and mimic AppKit's very clean layering system. We can do this using double buffering (which is essentially what AppKit does behind the scenes) by rendering all our objects to a hidden device context, and then pushing it all to the screen in one go.
*/
struct Win32Context
{
HWND hwnd;
HDC memDC;
HBITMAP backBuffer;
HBITMAP oldBitmap;
};
/*
The window creation and event loop is mostly pulled directly from Microsoft's official Windows documentation
*/
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
case WM_DESTROY:
if (g_customFontFamily)
delete g_customFontFamily;
if (g_fontCollection)
delete g_fontCollection;
Gdiplus::GdiplusShutdown(gdiplusToken);
PostQuitMessage(0);
return 0;
case WM_PAINT:
{
PAINTSTRUCT ps;
BeginPaint(hwnd, &ps);
EndPaint(hwnd, &ps);
return 0;
}
// Scroll wheel tracking. Mirrors the Mac implementation in window.mm, which keeps a
// rolling total in zoom_level via zoom_level += scrollingDeltaY * SCROLL_ZOOM_FACTOR.
// The Window* is retrieved from GWLP_USERDATA (set in the constructor) so this static
// callback can reach the instance. Windows reports the wheel delta as multiples of
// WHEEL_DELTA (120 per notch), so we normalise by it to get the same per-notch feel
// as Mac's scrollingDeltaY before applying the identical SCROLL_ZOOM_FACTOR.
case WM_MOUSEWHEEL:
{
Window *self = reinterpret_cast<Window *>(GetWindowLongPtr(hwnd, GWLP_USERDATA));
if (self)
{
int delta = GET_WHEEL_DELTA_WPARAM(wParam);
if (delta != 0)
{
self->zoom_level -= (delta / static_cast<float>(WHEEL_DELTA)) * SCROLL_ZOOM_FACTOR * 15;
}
}
return 0;
}
}
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
/*
Helper function to convert std::string type to a wide string literal used by the Win32 API
*/
std::wstring ToWideString(const std::string &narrow)
{
if (narrow.empty())
return L"";
// Get required size
int size_needed = MultiByteToWideChar(CP_UTF8, 0, narrow.c_str(), (int)narrow.length(), NULL, 0);
// Initialise the new wide string
std::wstring wide(size_needed, 0);
// Perform conversion
MultiByteToWideChar(CP_UTF8, 0, narrow.c_str(), (int)narrow.length(), &wide[0], size_needed);
return wide;
}
/*
The colour conversion to the Gdiplus::Color object used by GDI+.
Uses bitwise shifting to isolate each 8-bit channel from the 0xRRGGBBAA hex value.
(Native Win32 GDI calls can obtain a COLORREF from the result via .ToCOLORREF().)
*/
static Gdiplus::Color convertColor(std::uint64_t hexColor)
{
BYTE r = (hexColor >> 24) & 0xFF;
BYTE g = (hexColor >> 16) & 0xFF;
BYTE b = (hexColor >> 8) & 0xFF;
BYTE a = hexColor & 0xFF;
return Gdiplus::Color(a, r, g, b);
}
Window::Window(int _width, int _height, std::string _title) : width(_width), height(_height), title(_title)
{
Gdiplus::GdiplusStartupInput gdiplusStartupInput;
Gdiplus::GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);
HINSTANCE hInstance = GetModuleHandle(NULL);
const wchar_t CLASS_NAME[] = L"Sample Window Class";
// Register the window class
WNDCLASS wc = {};
wc.lpfnWndProc = WindowProc;
wc.hInstance = hInstance;
wc.lpszClassName = CLASS_NAME;
RegisterClass(&wc);
// Create the window
HWND hwnd = CreateWindowEx(
0, // Optional window styles
CLASS_NAME, // The class
ToWideString(title).c_str(), // Title
WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX, // Style (title bar, border, etc.)
CW_USEDEFAULT, CW_USEDEFAULT, // Position x, y
width, height, // Size - width, height
NULL, // Parent window
NULL, // Menu
hInstance, // Instance handle
NULL // Additional data
);
// Force the window to match the exact requested dimensions.
// CreateWindowEx sizes the whole window including the title bar/border, so we grow
// the rect by the non-client area (AdjustWindowRect) to keep the client area at width x height.
RECT rect = {0, 0, width, height};
AdjustWindowRect(&rect, WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX, FALSE);
SetWindowPos(hwnd, NULL, 0, 0, rect.right - rect.left, rect.bottom - rect.top, SWP_NOMOVE | SWP_NOZORDER);
// Associate this Window instance with the HWND so the static WindowProc can reach it
// (needed for scroll wheel tracking). Equivalent in spirit to the Mac delegate association.
SetWindowLongPtr(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(this));
// Make visible
ShowWindow(hwnd, SW_SHOW);
// Initialise double buffering (mimicking AppKit layers).
// We render everything to an off-screen memory DC (memDC) backed by backBuffer,
// then blit the whole thing to the window in one go (see process_events) to avoid flicker.
Win32Context *ctx = new Win32Context();
ctx->hwnd = hwnd;
HDC hdc = GetDC(hwnd);
ctx->memDC = CreateCompatibleDC(hdc);
ctx->backBuffer = CreateCompatibleBitmap(hdc, width, height);
// SelectObject returns the DC's default 1x1 bitmap; we keep it in oldBitmap to restore later
ctx->oldBitmap = (HBITMAP)SelectObject(ctx->memDC, ctx->backBuffer);
ReleaseDC(hwnd, hdc);
_window = (void *)ctx;
is_open = true;
load_font(GLOBAL_FONT + ".ttf");
}
void Window::setup_input_listeners()
{
// Don't need this. Windows mouse tracking is more lightweight/efficient so can run inside process_events()
}
bool Window::load_font(const std::string &file_path)
{
// Initialize the collection if it hasn't been created yet
if (!g_fontCollection)
{
g_fontCollection = new Gdiplus::PrivateFontCollection();
}
std::wstring w_path = ToWideString(file_path);
// Load the .ttf file directly into the GDI+ collection
if (g_fontCollection->AddFontFile(w_path.c_str()) != Gdiplus::Ok)
{
std::cerr << "Failed to load font: " << file_path << std::endl;
return false;
}
// Cache the Font Family object so drawing text is extremely fast
std::wstring wfont = ToWideString(GLOBAL_FONT);
g_customFontFamily = new Gdiplus::FontFamily(wfont.c_str(), g_fontCollection);
return true;
}
void Window::process_events()
{
Win32Context *ctx = static_cast<Win32Context *>(_window);
// Emulate NSApp updatewindows by pushing back buffer to screen
HDC hdc = GetDC(ctx->hwnd);
BitBlt(hdc, 0, 0, width, height, ctx->memDC, 0, 0, SRCCOPY);
ReleaseDC(ctx->hwnd, hdc);
// Non-blocking message pump
MSG msg = {};
while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
if (msg.message == WM_QUIT)
{
is_open = false;
}
TranslateMessage(&msg);
DispatchMessage(&msg);
}
if (!IsWindow(ctx->hwnd))
{
is_open = false;
return;
}
// Mouse tracking: fetch the global cursor position then convert it into window-local pixels
POINT pt;
GetCursorPos(&pt);
ScreenToClient(ctx->hwnd, &pt);
mouse_position.x = pt.x;
mouse_position.y = pt.y;
// The high bit of GetAsyncKeyState is set while the left button is physically down
is_mouse_down = (GetAsyncKeyState(VK_LBUTTON) & 0x8000) != 0;
}
void Window::clear_screen(std::uint64_t color)
{
Win32Context *ctx = static_cast<Win32Context *>(_window);
RECT rect = {0, 0, width, height};
HBRUSH brush = CreateSolidBrush(convertColor(color).ToCOLORREF());
FillRect(ctx->memDC, &rect, brush);
DeleteObject(brush);
}
void Window::fill_rectangle(int x, int y, int w, int h, Color color, bool is_button)
{
Win32Context *ctx = static_cast<Win32Context *>(_window);
// Attach GDI+ graphics context
Gdiplus::Graphics graphics(ctx->memDC);
graphics.SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias);
if (is_button)
{
int diameter = 12;
int radius = diameter / 2;
// Helper lambda to construct a rounded rectangle path for GDI+
auto add_round_rect = [](Gdiplus::GraphicsPath &path, int rx, int ry, int rw, int rh, int rr)
{
path.AddArc(rx, ry, rr * 2, rr * 2, 180, 90);
path.AddArc(rx + rw - rr * 2, ry, rr * 2, rr * 2, 270, 90);
path.AddArc(rx + rw - rr * 2, ry + rh - rr * 2, rr * 2, rr * 2, 0, 90);
path.AddArc(rx, ry + rh - rr * 2, rr * 2, rr * 2, 90, 90);
path.CloseFigure();
};
// Soft, fading drop shadow
// Stacking 3 semi-transparent layers creates a pseudo-Gaussian blur effect
for (int i = 0; i < 3; ++i)
{
Gdiplus::GraphicsPath shadowPath;
// Shift slightly down and right, expanding outwards each iteration
add_round_rect(shadowPath, x + 1 - i, y + 2 - i, w + (i * 2), h + (i * 2), radius + i);
// Alpha channel decreases (fades out) as the shadow expands
Gdiplus::SolidBrush shadowBrush(Gdiplus::Color(40 - (i * 12), 0, 0, 0));
graphics.FillPath(&shadowBrush, &shadowPath);
}
// Build the main button path
Gdiplus::GraphicsPath buttonPath;
add_round_rect(buttonPath, x, y, w, h, radius);
// Draw the button face
Gdiplus::SolidBrush buttonBrush(convertColor(color));
graphics.FillPath(&buttonBrush, &buttonPath);
}
else
{
// Standard, non-button rectangle rendering
Gdiplus::SolidBrush brush(convertColor(color));
graphics.FillRectangle(&brush, x, y, w, h);
}
}
void Window::fill_circle(int x, int y, int radius, Color color)
{
Win32Context *ctx = static_cast<Win32Context *>(_window);
// Activate the sub-pixel edge smoothing to mimic MacOS behaviour
Gdiplus::Graphics graphics(ctx->memDC);
graphics.SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias);
Gdiplus::SolidBrush brush(convertColor(color));
graphics.FillEllipse(&brush, x - radius, y - radius, radius * 2, radius * 2);
}
void Window::draw_line(int x1, int y1, int x2, int y2, Color color, int linewidth)
{
Win32Context *ctx = static_cast<Win32Context *>(_window);
Gdiplus::Graphics graphics(ctx->memDC);
graphics.SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias);
Gdiplus::Pen pen(convertColor(color), static_cast<Gdiplus::REAL>(linewidth));
graphics.DrawLine(&pen, x1, y1, x2, y2);
}
void Window::draw_text(const std::string &text, int x, int y, double size, Color color, int box_width, int box_height)
{
Win32Context *ctx = static_cast<Win32Context *>(_window);
// Attach GDI+ graphics context
Gdiplus::Graphics graphics(ctx->memDC);
// Set text anti-aliasing to match macOS CoreGraphics smooth font rendering
graphics.SetTextRenderingHint(Gdiplus::TextRenderingHintClearTypeGridFit);
// Construct the font directly from the cached GDI+ font family
Gdiplus::Font gdiplusFont(
g_customFontFamily,
static_cast<Gdiplus::REAL>(size),
Gdiplus::FontStyleRegular,
Gdiplus::UnitPixel // Ensures size parameter acts as raw logical pixels (mimics Mac)
);
// Setup the text color brush
Gdiplus::SolidBrush textBrush(convertColor(color));
// Setup the text formatting and alignment
Gdiplus::StringFormat format;
format.SetAlignment(Gdiplus::StringAlignmentCenter);
format.SetLineAlignment(Gdiplus::StringAlignmentCenter); // Vertically centre within layoutRect
// NoClip lets glyphs overhang the layout rect instead of being clipped.
// GDI+ reserves ~1/6 em of padding around a string and clips to the rect by
// default, which cuts the trailing glyph when text
// is centered in a tight box. macOS CoreText doesn't do this, hence Mac-only render.
format.SetFormatFlags(Gdiplus::StringFormatFlagsNoWrap | Gdiplus::StringFormatFlagsNoClip);
// Define the floating-point bounding box
Gdiplus::RectF layoutRect(
static_cast<float>(x),
static_cast<float>(y),
static_cast<float>(box_width),
static_cast<float>(box_height));
std::wstring wtext = ToWideString(text);
// Render the text seamlessly to the double-buffer
graphics.DrawString(wtext.c_str(), -1, &gdiplusFont, layoutRect, &format, &textBrush);
}
#endif