1#include "taichi/common/core.h"
2
3#if defined(TI_PLATFORM_WINDOWS)
4#include <windowsx.h>
5#include "taichi/common/task.h"
6#include "taichi/ui/gui/gui.h"
7#include <cctype>
8#include <map>
9
10#include <Windows.h>
11#include <string>
12#include <stdint.h>
13#include <stdexcept>
14
15// Note: some code is copied from MSDN:
16// https://docs.microsoft.com/en-us/windows/desktop/learnwin32/introduction-to-windows-programming-in-c--
17
18std::map<HWND, taichi::GUI *> gui_from_hwnd;
19
20static std::string lookup_keysym(WPARAM wParam, LPARAM lParam) {
21 int key = wParam;
22 switch (key) {
23 /*** http://kbdedit.com/manual/low_level_vk_list.html ***/
24 case VK_LEFT:
25 return "Left";
26 case VK_RIGHT:
27 return "Right";
28 case VK_UP:
29 return "Up";
30 case VK_DOWN:
31 return "Down";
32 case VK_TAB:
33 return "Tab";
34 case VK_RETURN:
35 return "Return";
36 case VK_BACK:
37 return "BackSpace";
38 case VK_ESCAPE:
39 return "Escape";
40 case VK_SHIFT:
41 case VK_LSHIFT:
42 return "Shift_L";
43 case VK_RSHIFT:
44 return "Shift_R";
45 case VK_CONTROL:
46 case VK_LCONTROL:
47 return "Control_L";
48 case VK_RCONTROL:
49 return "Control_R";
50 case VK_MENU:
51 case VK_LMENU:
52 return "Alt_L";
53 case VK_RMENU:
54 return "Alt_R";
55 case VK_CAPITAL:
56 return "Caps_Lock";
57 /*** TODO: win32 keyboard WIP, add more cases, match XKeysymToString() ***/
58 default:
59 if (VK_F1 <= key && key <= VK_F12)
60 return fmt::format("F{}", key - VK_F1);
61 else if (isascii(key))
62 return std::string(1, std::tolower(key));
63 else
64 return fmt::format("Vk{}", key);
65 }
66}
67
68LRESULT CALLBACK WindowProc(HWND hwnd,
69 UINT uMsg,
70 WPARAM wParam,
71 LPARAM lParam) {
72 auto dc = GetDC(hwnd);
73 auto gui = gui_from_hwnd[hwnd];
74 using namespace taichi;
75 POINT p{};
76 switch (uMsg) {
77 case WM_LBUTTONDOWN:
78 gui->mouse_event(
79 GUI::MouseEvent{GUI::MouseEvent::Type::press, gui->cursor_pos});
80 gui->key_events.push_back(
81 GUI::KeyEvent{GUI::KeyEvent::Type::press, "LMB", gui->cursor_pos});
82 break;
83 case WM_LBUTTONUP:
84 gui->mouse_event(
85 GUI::MouseEvent{GUI::MouseEvent::Type::release, gui->cursor_pos});
86 gui->key_events.push_back(
87 GUI::KeyEvent{GUI::KeyEvent::Type::release, "LMB", gui->cursor_pos});
88 break;
89 case WM_MBUTTONDOWN:
90 gui->key_events.push_back(
91 GUI::KeyEvent{GUI::KeyEvent::Type::press, "MMB", gui->cursor_pos});
92 break;
93 case WM_MBUTTONUP:
94 gui->key_events.push_back(
95 GUI::KeyEvent{GUI::KeyEvent::Type::release, "MMB", gui->cursor_pos});
96 break;
97 case WM_RBUTTONDOWN:
98 gui->key_events.push_back(
99 GUI::KeyEvent{GUI::KeyEvent::Type::press, "RMB", gui->cursor_pos});
100 break;
101 case WM_RBUTTONUP:
102 gui->key_events.push_back(
103 GUI::KeyEvent{GUI::KeyEvent::Type::release, "RMB", gui->cursor_pos});
104 break;
105 case WM_MOUSEMOVE:
106 p = {GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)};
107 gui->set_mouse_pos(p.x, gui->height - 1 - p.y);
108 gui->key_events.push_back(
109 GUI::KeyEvent{GUI::KeyEvent::Type::move, "Motion", gui->cursor_pos});
110 gui->mouse_event(
111 GUI::MouseEvent{GUI::MouseEvent::Type::move, gui->cursor_pos});
112 break;
113 case WM_MOUSEWHEEL:
114 p = {GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)};
115 ScreenToClient(hwnd, &p);
116 gui->set_mouse_pos(p.x, gui->height - 1 - p.y);
117 gui->key_events.push_back(
118 GUI::KeyEvent{GUI::KeyEvent::Type::move, "Wheel", gui->cursor_pos,
119 Vector2i{0, GET_WHEEL_DELTA_WPARAM(wParam)}});
120 break;
121 case WM_MOUSEHWHEEL:
122 p = {GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)};
123 ScreenToClient(hwnd, &p);
124 gui->set_mouse_pos(p.x, gui->height - 1 - p.y);
125 gui->key_events.push_back(
126 GUI::KeyEvent{GUI::KeyEvent::Type::move, "Wheel", gui->cursor_pos,
127 Vector2i{GET_WHEEL_DELTA_WPARAM(wParam), 0}});
128 break;
129 case WM_PAINT:
130 break;
131 case WM_KEYDOWN:
132 gui->key_pressed = true;
133 gui->key_events.push_back(GUI::KeyEvent{GUI::KeyEvent::Type::press,
134 lookup_keysym(wParam, lParam),
135 gui->cursor_pos});
136 break;
137 case WM_KEYUP:
138 gui->key_events.push_back(GUI::KeyEvent{GUI::KeyEvent::Type::release,
139 lookup_keysym(wParam, lParam),
140 gui->cursor_pos});
141 break;
142 case WM_CLOSE:
143 // https://stackoverflow.com/questions/3155782/what-is-the-difference-between-wm-quit-wm-close-and-wm-destroy-in-a-windows-pr
144 gui->send_window_close_message();
145 break;
146 }
147 return DefWindowProc(hwnd, uMsg, wParam, lParam);
148}
149
150namespace taichi {
151
152void GUI::process_event() {
153 MSG msg;
154 if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
155 TranslateMessage(&msg);
156 DispatchMessage(&msg);
157 }
158}
159
160// From:
161// https://docs.microsoft.com/en-us/archive/msdn-magazine/2016/september/c-unicode-encoding-conversions-with-stl-strings-and-win32-apis
162
163// Represents an error during UTF-8 encoding conversions
164class Utf8ConversionException : public std::runtime_error {
165 private:
166 uint32_t _error_code;
167
168 public:
169 Utf8ConversionException(const char *message, uint32_t error_code)
170 : std::runtime_error(message), _error_code(error_code) {
171 }
172
173 uint32_t error_code() const {
174 return _error_code;
175 }
176};
177
178std::wstring utf8_to_utf16(const std::string &utf8) {
179 std::wstring utf16; // Result
180 if (utf8.empty()) {
181 return utf16;
182 }
183
184 // Safely fails if an invalid UTF-8 character
185 // is encountered in the input string
186 constexpr DWORD kFlags = MB_ERR_INVALID_CHARS;
187
188 if (utf8.length() > static_cast<size_t>((std::numeric_limits<int>::max)())) {
189 throw std::overflow_error(
190 "Input string too long: size_t-length doesn't fit into int.");
191 }
192
193 // Safely convert from size_t (STL string's length)
194 // to int (for Win32 APIs)
195 const int utf8_length = static_cast<int>(utf8.length());
196 const int utf16_length = ::MultiByteToWideChar(
197 CP_UTF8, // Source string is in UTF-8
198 kFlags, // Conversion flags
199 utf8.data(), // Source UTF-8 string pointer
200 utf8_length, // Length of the source UTF-8 string, in chars
201 nullptr, // Unused - no conversion done in this step
202 0 // Request size of destination buffer, in wchar_ts
203 );
204 if (utf16_length == 0) {
205 // Conversion error: capture error code and throw
206 const DWORD error = ::GetLastError();
207 throw Utf8ConversionException(
208 "Cannot get result string length when converting "
209 "from UTF-8 to UTF-16 (MultiByteToWideChar failed).",
210 error);
211 }
212 utf16.resize(utf16_length);
213
214 // Convert from UTF-8 to UTF-16
215 int result = ::MultiByteToWideChar(
216 CP_UTF8, // Source string is in UTF-8
217 kFlags, // Conversion flags
218 utf8.data(), // Source UTF-8 string pointer
219 utf8_length, // Length of source UTF-8 string, in chars
220 &utf16[0], // Pointer to destination buffer
221 utf16_length // Size of destination buffer, in wchar_ts
222 );
223
224 if (result == 0) {
225 // Conversion error: capture error code and throw
226 const DWORD error = ::GetLastError();
227 throw Utf8ConversionException(
228 "Cannot convert from UTF-8 to UTF-16 "
229 "(MultiByteToWideChar failed).",
230 error);
231 }
232
233 return utf16;
234}
235
236void GUI::create_window() {
237 static LPCWSTR CLASS_NAME = L"Taichi Win32 Window";
238
239 DWORD dwVersion = 0;
240 DWORD dwMajorVersion = 0;
241 DWORD dwMinorVersion = 0;
242
243 dwVersion = GetVersion();
244
245 dwMajorVersion = (DWORD)(LOBYTE(LOWORD(dwVersion)));
246 dwMinorVersion = (DWORD)(HIBYTE(LOWORD(dwVersion)));
247
248 WNDCLASSW wc = {};
249
250 wc.lpfnWndProc = WindowProc;
251 wc.hInstance = GetModuleHandleA(0);
252 wc.lpszClassName = CLASS_NAME;
253
254 RegisterClassW(&wc);
255
256 RECT window_rect;
257 window_rect.left = 0;
258 window_rect.right = width;
259 window_rect.top = 0;
260 window_rect.bottom = height;
261
262 AdjustWindowRect(&window_rect, WS_OVERLAPPEDWINDOW, false);
263
264 std::wstring w_name = utf8_to_utf16(window_name);
265
266 hwnd = CreateWindowExW(0, // Optional window styles.
267 CLASS_NAME, // Window class
268 w_name.c_str(), // Window text
269 WS_OVERLAPPEDWINDOW, // Window style
270 // Size and position
271 CW_USEDEFAULT, CW_USEDEFAULT,
272 window_rect.right - window_rect.left,
273 window_rect.bottom - window_rect.top,
274 NULL, // Parent window
275 NULL, // Menu
276 GetModuleHandleA(0), // Instance handle
277 NULL // Additional application data
278 );
279 TI_ERROR_IF(hwnd == NULL, "Window creation failed");
280 gui_from_hwnd[hwnd] = this;
281
282 if (fullscreen) {
283 // https://www.cnblogs.com/lidabo/archive/2012/07/17/2595452.html
284 LONG style = GetWindowLong(hwnd, GWL_STYLE);
285 style &= ~WS_CAPTION & ~WS_SIZEBOX;
286 SetWindowLongW(hwnd, GWL_STYLE, style);
287 SetWindowPos(hwnd, NULL, 0, 0, GetSystemMetrics(SM_CXSCREEN),
288 GetSystemMetrics(SM_CYSCREEN), SWP_NOZORDER);
289 }
290
291 ShowWindow(hwnd, SW_SHOWDEFAULT);
292 hdc = GetDC(hwnd);
293 data = (COLORREF *)calloc(width * height, sizeof(COLORREF));
294 src = CreateCompatibleDC(hdc);
295}
296
297void GUI::redraw() {
298 UpdateWindow(hwnd);
299 if (!fast_gui) {
300 // http://www.cplusplus.com/reference/cstdlib/calloc/
301 for (int i = 0; i < width; i++) {
302 for (int j = 0; j < height; j++) {
303 auto c = reinterpret_cast<unsigned char *>(data + (j * width) + i);
304 auto d = canvas->img[i][height - j - 1];
305 c[0] = uint8(clamp(int(d[2] * 255.0_f), 0, 255));
306 c[1] = uint8(clamp(int(d[1] * 255.0_f), 0, 255));
307 c[2] = uint8(clamp(int(d[0] * 255.0_f), 0, 255));
308 c[3] = 0;
309 }
310 }
311 }
312 bitmap = CreateBitmap(width, height, 1, 32,
313 fast_gui ? (void *)fast_buf : (void *)data);
314 SelectObject(src, bitmap);
315 BitBlt(hdc, 0, 0, width, height, src, 0, 0, SRCCOPY);
316 DeleteObject(bitmap);
317}
318
319void GUI::set_title(std::string title) {
320 std::wstring w_title = utf8_to_utf16(title);
321 SetWindowTextW(hwnd, w_title.c_str());
322}
323
324GUI::~GUI() {
325 if (show_gui) {
326 std::free(data);
327 DeleteDC(src);
328 DestroyWindow(hwnd);
329 gui_from_hwnd.erase(hwnd);
330 }
331}
332
333} // namespace taichi
334
335#endif
336