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 | |
18 | std::map<HWND, taichi::GUI *> gui_from_hwnd; |
19 | |
20 | static 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 | |
68 | LRESULT 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 | |
150 | namespace taichi { |
151 | |
152 | void 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 |
164 | class 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 | |
178 | std::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 | |
236 | void 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 | |
297 | void 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 | |
319 | void GUI::set_title(std::string title) { |
320 | std::wstring w_title = utf8_to_utf16(title); |
321 | SetWindowTextW(hwnd, w_title.c_str()); |
322 | } |
323 | |
324 | GUI::~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 | |