HikoGUI
A low latency retained GUI
Loading...
Searching...
No Matches
gui_window_win32.hpp
1// Copyright Take Vos 2019, 2021-2022.
2// Distributed under the Boost Software License, Version 1.0.
3// (See accompanying file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt)
4
5#pragma once
6
8
9#include "gui_event.hpp"
10#include "gui_window_size.hpp"
11#include "hitbox.hpp"
12#include "keyboard_bindings.hpp"
13#include "theme_book.hpp"
14#include "widget_intf.hpp"
15#include "mouse_cursor.hpp"
16#include "../GFX/GFX.hpp"
17#include "../crt/crt.hpp"
18#include "../macros.hpp"
19#include <unordered_map>
20
21hi_export_module(hikogui.GUI : gui_window);
22
23hi_export namespace hi::inline v1 {
24
26public:
27 HWND win32Window = nullptr;
28
30
40 aarectangle rectangle;
41
49 mouse_cursor current_mouse_cursor = mouse_cursor::None;
50
55 bool resizing = false;
56
61 hi::pixel_density pixel_density = {pixels_per_inch(96.0f), device_type::desktop};
62
66 hi::theme theme = {};
67
70 extent2 widget_size;
71
76
77 gui_window(gui_window const&) = delete;
78 gui_window& operator=(gui_window const&) = delete;
79 gui_window(gui_window&&) = delete;
80 gui_window& operator=(gui_window&&) = delete;
81
82 gui_window(std::unique_ptr<widget_intf> widget) noexcept : _widget(std::move(widget)), track_mouse_leave_event_parameters()
83 {
84 if (_first_window) {
85 if (not os_settings::start_subsystem()) {
86 hi_log_fatal("Could not start the os_settings subsystem.");
87 }
88
89 register_font_file(URL{"resource:elusiveicons-webfont.ttf"});
90 register_font_file(URL{"resource:hikogui_icons.ttf"});
91 register_font_directories(font_dirs());
92
93 register_theme_directories(theme_dirs());
94
95 try {
96 load_system_keyboard_bindings(URL{"resource:win32.keybinds.json"});
97 } catch (std::exception const& e) {
98 hi_log_fatal("Could not load keyboard bindings. \"{}\"", e.what());
99 }
100
101 _first_window = true;
102 }
103
104 SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
105
106 _widget->set_window(this);
107
108 // Execute a constraint check to determine initial window size.
109 theme = get_selected_theme().transform(pixel_density);
110
111 _widget_constraints = _widget->update_constraints();
112 auto const new_size = _widget_constraints.preferred;
113
114 // Reset the keyboard target to not focus anything.
115 update_keyboard_target({});
116
117 // For changes in setting on the OS we should reconstrain/layout/redraw the window
118 // For example when the language or theme changes.
119 _setting_change_cbt = os_settings::subscribe(
120 [this] {
121 ++global_counter<"gui_window:os_setting:constrain">;
122 this->process_event({gui_event_type::window_reconstrain});
123 },
124 callback_flags::main);
125
126 // Subscribe on theme changes.
127 _selected_theme_cbt = theme_book::global().selected_theme.subscribe(
128 [this](auto...) {
129 ++global_counter<"gui_window:selected_theme:constrain">;
130 this->process_event({gui_event_type::window_reconstrain});
131 },
132 callback_flags::main);
133
134 _render_cbt = loop::main().subscribe_render([this](utc_nanoseconds display_time) {
135 this->render(display_time);
136 });
137
138 // Delegate has been called, layout of widgets has been calculated for the
139 // minimum and maximum size of the window.
140 create_window(new_size);
141 }
142
144 {
145 try {
146 if (win32Window != nullptr) {
147 DestroyWindow(win32Window);
148 hi_assert(win32Window == nullptr);
149 // hi_log_fatal("win32Window was not destroyed before Window '{}' was destructed.", title);
150 }
151
152 } catch (std::exception const& e) {
153 hi_log_fatal("Could not properly destruct gui_window. '{}'", e.what());
154 }
155
156 // Destroy the top-level widget, before Window-members that the widgets require from the window during their destruction.
157 _widget = {};
158
159 try {
160 surface.reset();
161 hi_log_info("Window '{}' has been properly destructed.", _title);
162
163 } catch (std::exception const& e) {
164 hi_log_fatal("Could not properly destruct gui_window. '{}'", e.what());
165 }
166 }
167
168 template<typename Widget>
169 [[nodiscard]] Widget& widget() const noexcept
170 {
171 return up_cast<Widget>(*_widget);
172 }
173
174 void set_title(label title) noexcept
175 {
176 _title = std::move(title);
177 }
178
182 void render(utc_nanoseconds display_time_point)
183 {
184 if (surface->device() == nullptr) {
185 // If there is no device configured for the surface don't try to render.
186 return;
187 }
188
189 auto const t1 = trace<"window::render">();
190
191 hi_axiom(loop::main().on_thread());
192 hi_assert_not_null(surface);
193 hi_assert_not_null(_widget);
194
195 // When a widget requests it or a window-wide event like language change
196 // has happened all the widgets will be set_constraints().
197 auto need_reconstrain = _reconstrain.exchange(false, std::memory_order_relaxed);
198
199#if 0
200 // For performance checks force reconstrain.
201 need_reconstrain = true;
202#endif
203
204 if (need_reconstrain) {
205 auto const t2 = trace<"window::constrain">();
206
207 theme = get_selected_theme().transform(pixel_density);
208
209 _widget_constraints = _widget->update_constraints();
210 }
211
212 // Check if the window size matches the preferred size of the window_widget.
213 // If not ask the operating system to change the size of the window, which is
214 // done asynchronously.
215 //
216 // We need to continue drawing into the incorrectly sized window, otherwise
217 // Vulkan will not detect the change of drawing surface's size.
218 //
219 // Make sure the widget does have its window rectangle match the constraints, otherwise
220 // the logic for layout and drawing becomes complicated.
221 if (_resize.exchange(false, std::memory_order::relaxed)) {
222 // If a widget asked for a resize, change the size of the window to the preferred size of the widgets.
223 auto const current_size = rectangle.size();
224 auto const new_size = _widget_constraints.preferred;
225 if (new_size != current_size) {
226 hi_log_info("A new preferred window size {} was requested by one of the widget.", new_size);
227 set_window_size(new_size);
228 }
229
230 } else {
231 // Check if the window size matches the minimum and maximum size of the widgets, otherwise resize.
232 auto const current_size = rectangle.size();
233 auto const new_size = clamp(current_size, _widget_constraints.minimum, _widget_constraints.maximum);
234 if (new_size != current_size and size_state() != gui_window_size::minimized) {
235 hi_log_info("The current window size {} must grow or shrink to {} to fit the widgets.", current_size, new_size);
236 set_window_size(new_size);
237 }
238 }
239
240 if (rectangle.size() < _widget_constraints.minimum or rectangle.size() > _widget_constraints.maximum) {
241 // Even after the resize above it is possible to have an incorrect window size.
242 // For example when minimizing the window.
243 // Stop processing rendering for this window here.
244 return;
245 }
246
247 // Update the graphics' surface to the current size of the window.
248 surface->update(rectangle.size());
249
250 // Make sure the widget's layout is updated before draw, but after window resize.
251 auto need_relayout = _relayout.exchange(false, std::memory_order_relaxed);
252
253#if 0
254 // For performance checks force relayout.
255 need_relayout = true;
256#endif
257
258 if (need_reconstrain or need_relayout or widget_size != rectangle.size()) {
259 auto const t2 = trace<"window::layout">();
260 widget_size = rectangle.size();
261
262 // Guarantee that the layout size is always at least the minimum size.
263 // We do this because it simplifies calculations if no minimum checks are necessary inside widget.
264 auto const widget_layout_size = max(_widget_constraints.minimum, widget_size);
265 _widget->set_layout(widget_layout{widget_layout_size, _size_state, subpixel_orientation(), display_time_point});
266
267 // After layout do a complete redraw.
268 _redraw_rectangle = aarectangle{widget_size};
269 }
270
271#if 0
272 // For performance checks force redraw.
273 _redraw_rectangle = aarectangle{widget_size};
274#endif
275
276 // Draw widgets if the _redraw_rectangle was set.
277 if (auto draw_context = surface->render_start(_redraw_rectangle)) {
278 _redraw_rectangle = aarectangle{};
279 draw_context.display_time_point = display_time_point;
280 draw_context.subpixel_orientation = subpixel_orientation();
281 draw_context.saturation = 1.0f;
282
283 {
284 auto const t2 = trace<"window::draw">();
285 _widget->draw(draw_context);
286 }
287 {
288 auto const t2 = trace<"window::submit">();
289 surface->render_finish(draw_context);
290 }
291 }
292 }
293
296 void set_cursor(mouse_cursor cursor) noexcept
297 {
298 hi_axiom(loop::main().on_thread());
299
300 if (current_mouse_cursor == cursor) {
301 return;
302 }
303 current_mouse_cursor = cursor;
304
305 if (cursor == mouse_cursor::None) {
306 return;
307 }
308
309 static auto idcAppStarting = LoadCursorW(nullptr, IDC_APPSTARTING);
310 static auto idcArrow = LoadCursorW(nullptr, IDC_ARROW);
311 static auto idcHand = LoadCursorW(nullptr, IDC_HAND);
312 static auto idcIBeam = LoadCursorW(nullptr, IDC_IBEAM);
313 static auto idcNo = LoadCursorW(nullptr, IDC_NO);
314
315 auto idc = idcNo;
316 switch (cursor) {
317 case mouse_cursor::None:
318 idc = idcAppStarting;
319 break;
320 case mouse_cursor::Default:
321 idc = idcArrow;
322 break;
323 case mouse_cursor::Button:
324 idc = idcHand;
325 break;
326 case mouse_cursor::TextEdit:
327 idc = idcIBeam;
328 break;
329 default:
330 hi_no_default();
331 }
332
333 SetCursor(idc);
334 }
335
339 {
340 hi_axiom(loop::main().on_thread());
341 if (not PostMessageW(win32Window, WM_CLOSE, 0, 0)) {
342 hi_log_error("Could not send WM_CLOSE to window {}: {}", _title, get_last_error_message());
343 }
344 }
345
351 void set_size_state(gui_window_size state) noexcept
352 {
353 hi_axiom(loop::main().on_thread());
354
355 if (_size_state == state) {
356 return;
357 }
358
359 if (_size_state == gui_window_size::normal) {
360 _restore_rectangle = rectangle;
361 } else if (_size_state == gui_window_size::minimized) {
362 ShowWindow(win32Window, SW_RESTORE);
363 _size_state = gui_window_size::normal;
364 }
365
366 if (state == gui_window_size::normal) {
367 auto const left = round_cast<int>(_restore_rectangle.left());
368 auto const top = round_cast<int>(_restore_rectangle.top());
369 auto const width = round_cast<int>(_restore_rectangle.width());
370 auto const height = round_cast<int>(_restore_rectangle.height());
371 auto const inv_top = round_cast<int>(os_settings::primary_monitor_rectangle().height()) - top;
372 SetWindowPos(win32Window, HWND_TOP, left, inv_top, width, height, 0);
373 _size_state = gui_window_size::normal;
374
375 } else if (state == gui_window_size::minimized) {
376 ShowWindow(win32Window, SW_MINIMIZE);
377 _size_state = gui_window_size::minimized;
378
379 } else if (state == gui_window_size::maximized) {
380 auto const workspace = workspace_rectangle();
381 auto const max_size = _widget_constraints.maximum;
382
383 // Try to resize the window while keeping the toolbar in the same location.
384 auto const width = std::min(max_size.width(), workspace.width());
385 auto const height = std::min(max_size.height(), workspace.height());
386 auto const left = std::clamp(rectangle.left(), workspace.left(), workspace.right() - width);
387 auto const top = std::clamp(rectangle.top(), workspace.bottom() + height, workspace.top());
388 auto const inv_top = os_settings::primary_monitor_rectangle().height() - top;
389 SetWindowPos(
390 win32Window,
391 HWND_TOP,
392 round_cast<int>(left),
393 round_cast<int>(inv_top),
394 round_cast<int>(width),
395 round_cast<int>(height),
396 0);
397 _size_state = gui_window_size::maximized;
398
399 } else if (state == gui_window_size::fullscreen) {
400 auto const fullscreen = fullscreen_rectangle();
401 auto const max_size = _widget_constraints.maximum;
402 if (fullscreen.width() > max_size.width() or fullscreen.height() > max_size.height()) {
403 // Do not go full screen if the widget is unable to go that large.
404 return;
405 }
406
407 auto const left = round_cast<int>(fullscreen.left());
408 auto const top = round_cast<int>(fullscreen.top());
409 auto const width = round_cast<int>(fullscreen.width());
410 auto const height = round_cast<int>(fullscreen.height());
411 auto const inv_top = round_cast<int>(os_settings::primary_monitor_rectangle().height()) - top;
412 SetWindowPos(win32Window, HWND_TOP, left, inv_top, width, height, 0);
413 _size_state = gui_window_size::fullscreen;
414 }
415 }
416
419 [[nodiscard]] aarectangle workspace_rectangle() const noexcept
420 {
421 auto const monitor = MonitorFromWindow(win32Window, MONITOR_DEFAULTTOPRIMARY);
422 if (monitor == NULL) {
423 hi_log_error("Could not get monitor for the window.");
424 return {0, 0, 1920, 1080};
425 }
426
427 MONITORINFO info;
428 info.cbSize = sizeof(MONITORINFO);
429 if (not GetMonitorInfo(monitor, &info)) {
430 hi_log_error("Could not get monitor info for the window.");
431 return {0, 0, 1920, 1080};
432 }
433
434 auto const left = narrow_cast<float>(info.rcWork.left);
435 auto const top = narrow_cast<float>(info.rcWork.top);
436 auto const right = narrow_cast<float>(info.rcWork.right);
437 auto const bottom = narrow_cast<float>(info.rcWork.bottom);
438 auto const width = right - left;
439 auto const height = bottom - top;
440 auto const inv_bottom = os_settings::primary_monitor_rectangle().height() - bottom;
441 return aarectangle{left, inv_bottom, width, height};
442 }
443
446 [[nodiscard]] aarectangle fullscreen_rectangle() const noexcept
447 {
448 auto const monitor = MonitorFromWindow(win32Window, MONITOR_DEFAULTTOPRIMARY);
449 if (monitor == NULL) {
450 hi_log_error("Could not get monitor for the window.");
451 return {0, 0, 1920, 1080};
452 }
453
454 MONITORINFO info;
455 info.cbSize = sizeof(MONITORINFO);
456 if (not GetMonitorInfo(monitor, &info)) {
457 hi_log_error("Could not get monitor info for the window.");
458 return {0, 0, 1920, 1080};
459 }
460
461 auto const left = narrow_cast<float>(info.rcMonitor.left);
462 auto const top = narrow_cast<float>(info.rcMonitor.top);
463 auto const right = narrow_cast<float>(info.rcMonitor.right);
464 auto const bottom = narrow_cast<float>(info.rcMonitor.bottom);
465 auto const width = right - left;
466 auto const height = bottom - top;
467 auto const inv_bottom = os_settings::primary_monitor_rectangle().height() - bottom;
468 return aarectangle{left, inv_bottom, width, height};
469 }
470
473 gui_window_size size_state() const noexcept
474 {
475 return _size_state;
476 }
477
478 [[nodiscard]] hi::subpixel_orientation subpixel_orientation() const noexcept
479 {
480 // The table for viewing distance are:
481 //
482 // - Phone/Watch: 10 inch
483 // - Tablet: 15 inch
484 // - Notebook/Desktop: 20 inch
485 //
486 // Pixels Per Degree = PPD = 2 * viewing_distance * resolution * tan(0.5 degree)
487 constexpr auto tan_half_degree = 0.00872686779075879f;
488 constexpr auto viewing_distance = 20.0f;
489
490 auto const ppd = 2 * viewing_distance * pixel_density.ppi * tan_half_degree;
491
492 if (ppd > pixels_per_inch(55.0f)) {
493 // High resolution displays do not require subpixel-aliasing.
494 return hi::subpixel_orientation::unknown;
495 } else {
496 // The win32 API does not have a per-monitor subpixel-orientation.
497 return os_settings::subpixel_orientation();
498 }
499 }
500
506 {
507 hi_axiom(loop::main().on_thread());
508
509 // Position the system menu on the left side, below the system menu button.
510 auto const left = rectangle.left();
511 auto const top = rectangle.top() - 30.0f;
512
513 // Convert to y-axis down coordinate system
514 auto const inv_top = os_settings::primary_monitor_rectangle().height() - top;
515
516 // Open the system menu window and wait.
517 auto const system_menu = GetSystemMenu(win32Window, false);
518 auto const cmd =
519 TrackPopupMenu(system_menu, TPM_RETURNCMD, round_cast<int>(left), round_cast<int>(inv_top), 0, win32Window, NULL);
520 if (cmd > 0) {
521 SendMessage(win32Window, WM_SYSCOMMAND, narrow_cast<WPARAM>(cmd), LPARAM{0});
522 }
523 }
524
527 void set_window_size(extent2 new_extent)
528 {
529 hi_axiom(loop::main().on_thread());
530
531 RECT original_rect;
532 if (not GetWindowRect(win32Window, &original_rect)) {
533 hi_log_error("Could not get the window's rectangle on the screen.");
534 }
535
536 auto const new_width = round_cast<int>(new_extent.width());
537 auto const new_height = round_cast<int>(new_extent.height());
538 auto const new_x = os_settings::left_to_right() ? narrow_cast<int>(original_rect.left) :
539 narrow_cast<int>(original_rect.right - new_width);
540 auto const new_y = narrow_cast<int>(original_rect.top);
541
542 SetWindowPos(
543 win32Window,
544 HWND_NOTOPMOST,
545 new_x,
546 new_y,
547 new_width,
548 new_height,
549 SWP_NOACTIVATE | SWP_NOOWNERZORDER | SWP_NOREDRAW | SWP_DEFERERASE | SWP_NOCOPYBITS | SWP_FRAMECHANGED);
550 }
551
552 void update_mouse_target(widget_id new_target_id, point2 position = {}) noexcept
553 {
554 hi_axiom(loop::main().on_thread());
555
556 if (_mouse_target_id != 0) {
557 if (new_target_id == _mouse_target_id) {
558 // Focus does not change.
559 return;
560 }
561
562 // The mouse target needs to be updated, send exit to previous target.
563 send_events_to_widget(_mouse_target_id, std::vector{gui_event{gui_event_type::mouse_exit}});
564 }
565
566 if (new_target_id != 0) {
567 _mouse_target_id = new_target_id;
568 send_events_to_widget(new_target_id, std::vector{gui_event::make_mouse_enter(position)});
569 } else {
570 _mouse_target_id = {};
571 }
572 }
573
580 void update_keyboard_target(widget_id new_target_id, keyboard_focus_group group = keyboard_focus_group::normal) noexcept
581 {
582 hi_axiom(loop::main().on_thread());
583
584 auto new_target_widget = get_if(_widget.get(), new_target_id, false);
585
586 // Before we are going to make new_target_widget empty, due to the rules below;
587 // capture which parents there are.
588 auto new_target_parent_chain = new_target_widget ? new_target_widget->parent_chain() : std::vector<widget_id>{};
589
590 // If the new target widget does not accept focus, for example when clicking
591 // on a disabled widget, or empty part of a window.
592 // In that case no widget will get focus.
593 if (new_target_widget == nullptr or not new_target_widget->accepts_keyboard_focus(group)) {
594 new_target_widget = nullptr;
595 }
596
597 if (auto const *const keyboard_target_widget = get_if(_widget.get(), _keyboard_target_id, false)) {
598 // keyboard target still exists and visible.
599 if (new_target_widget == keyboard_target_widget) {
600 // Focus does not change.
601 return;
602 }
603
604 send_events_to_widget(_keyboard_target_id, std::vector{gui_event{gui_event_type::keyboard_exit}});
605 }
606
607 // Tell "escape" to all the widget that are not parents of the new widget
608 _widget->handle_event_recursive(gui_event_type::gui_cancel, new_target_parent_chain);
609
610 // Tell the new widget that keyboard focus was entered.
611 if (new_target_widget != nullptr) {
612 _keyboard_target_id = new_target_widget->id;
613 send_events_to_widget(_keyboard_target_id, std::vector{gui_event{gui_event_type::keyboard_enter}});
614 } else {
615 _keyboard_target_id = {};
616 }
617 }
618
627 void update_keyboard_target(widget_id start_widget, keyboard_focus_group group, keyboard_focus_direction direction) noexcept
628 {
629 hi_axiom(loop::main().on_thread());
630
631 if (auto tmp = _widget->find_next_widget(start_widget, group, direction); tmp != start_widget) {
632 update_keyboard_target(tmp, group);
633
634 } else if (group == keyboard_focus_group::normal) {
635 // Could not find a next widget, loop around.
636 // menu items should not loop back.
637 tmp = _widget->find_next_widget({}, group, direction);
638 update_keyboard_target(tmp, group);
639 }
640 }
641
650 {
651 return update_keyboard_target(_keyboard_target_id, group, direction);
652 }
653
661 [[nodiscard]] std::optional<gstring> get_text_from_clipboard() const noexcept
662 {
663 if (not OpenClipboard(win32Window)) {
664 // Another application could have the clipboard locked.
665 hi_log_info("Could not open win32 clipboard '{}'", get_last_error_message());
666 return std::nullopt;
667 }
668
669 auto const defer_CloseClipboard = defer([] {
670 CloseClipboard();
671 });
672
673 UINT format = 0;
674 while ((format = EnumClipboardFormats(format)) != 0) {
675 switch (format) {
676 case CF_TEXT:
677 case CF_OEMTEXT:
678 case CF_UNICODETEXT:
679 {
680 auto const cb_data = GetClipboardData(CF_UNICODETEXT);
681 if (cb_data == nullptr) {
682 hi_log_error("Could not get clipboard data: '{}'", get_last_error_message());
683 return std::nullopt;
684 }
685
686 auto const *const wstr_c = static_cast<wchar_t const *>(GlobalLock(cb_data));
687 if (wstr_c == nullptr) {
688 hi_log_error("Could not lock clipboard data: '{}'", get_last_error_message());
689 return std::nullopt;
690 }
691
692 auto const defer_GlobalUnlock = defer([cb_data] {
693 if (not GlobalUnlock(cb_data) and GetLastError() != ERROR_SUCCESS) {
694 hi_log_error("Could not unlock clipboard data: '{}'", get_last_error_message());
695 }
696 });
697
698 auto r = to_gstring(hi::to_string(std::wstring_view(wstr_c)));
699 hi_log_debug("get_text_from_clipboard '{}'", to_string(r));
700 return {std::move(r)};
701 }
702 break;
703
704 default:;
705 }
706 }
707
708 if (GetLastError() != ERROR_SUCCESS) {
709 hi_log_error("Could not enumerator clipboard formats: '{}'", get_last_error_message());
710 }
711
712 return std::nullopt;
713 }
714
720 void put_text_on_clipboard(gstring_view text) const noexcept
721 {
722 if (not OpenClipboard(win32Window)) {
723 // Another application could have the clipboard locked.
724 hi_log_info("Could not open win32 clipboard '{}'", get_last_error_message());
725 return;
726 }
727
728 auto const defer_CloseClipboard = defer([] {
729 CloseClipboard();
730 });
731
732 if (not EmptyClipboard()) {
733 hi_log_error("Could not empty win32 clipboard '{}'", get_last_error_message());
734 return;
735 }
736
737 auto wtext = hi::to_wstring(unicode_normalize(to_u32string(text), unicode_normalize_config::NFC_CRLF_noctr()));
738
739 auto wtext_handle = GlobalAlloc(GMEM_MOVEABLE, (wtext.size() + 1) * sizeof(wchar_t));
740 if (wtext_handle == nullptr) {
741 hi_log_error("Could not allocate clipboard data '{}'", get_last_error_message());
742 return;
743 }
744
745 auto const defer_GlobalFree([&wtext_handle] {
746 if (wtext_handle != nullptr) {
747 GlobalFree(wtext_handle);
748 }
749 });
750
751 {
752 auto wtext_c = static_cast<wchar_t *>(GlobalLock(wtext_handle));
753 if (wtext_c == nullptr) {
754 hi_log_error("Could not lock string data '{}'", get_last_error_message());
755 return;
756 }
757
758 auto const defer_GlobalUnlock = defer([wtext_handle] {
759 if (not GlobalUnlock(wtext_handle) and GetLastError() != ERROR_SUCCESS) {
760 hi_log_error("Could not unlock string data '{}'", get_last_error_message());
761 }
762 });
763
764 std::memcpy(wtext_c, wtext.c_str(), (wtext.size() + 1) * sizeof(wchar_t));
765 }
766
767 if (SetClipboardData(CF_UNICODETEXT, wtext_handle) == nullptr) {
768 hi_log_error("Could not set clipboard data '{}'", get_last_error_message());
769 return;
770 } else {
771 // Data was transferred to clipboard.
772 wtext_handle = nullptr;
773 }
774 }
775
776 [[nodiscard]] translate2 window_to_screen() const noexcept
777 {
778 return translate2{rectangle.left(), rectangle.bottom()};
779 }
780
781 [[nodiscard]] translate2 screen_to_window() const noexcept
782 {
783 return ~window_to_screen();
784 }
785
794 bool process_event(gui_event event) noexcept
795 {
796 using enum gui_event_type;
797
798 hi_axiom(loop::main().on_thread());
799
800 switch (event.type()) {
801 case window_redraw:
802 _redraw_rectangle.fetch_or(event.rectangle());
803 return true;
804
805 case window_relayout:
806 _relayout.store(true, std::memory_order_relaxed);
807 return true;
808
809 case window_reconstrain:
810 _reconstrain.store(true, std::memory_order_relaxed);
811 return true;
812
813 case window_resize:
814 _resize.store(true, std::memory_order_relaxed);
815 return true;
816
817 case window_minimize:
818 set_size_state(gui_window_size::minimized);
819 return true;
820
821 case window_maximize:
822 set_size_state(gui_window_size::maximized);
823 return true;
824
825 case window_normalize:
826 set_size_state(gui_window_size::normal);
827 return true;
828
829 case window_close:
830 close_window();
831 return true;
832
833 case window_open_sysmenu:
834 open_system_menu();
835 return true;
836
837 case window_set_keyboard_target:
838 {
839 auto const& target = event.keyboard_target();
840 if (target.widget_id == 0) {
841 update_keyboard_target(target.group, target.direction);
842 } else if (target.direction == keyboard_focus_direction::here) {
843 update_keyboard_target(target.widget_id, target.group);
844 } else {
845 update_keyboard_target(target.widget_id, target.group, target.direction);
846 }
847 }
848 return true;
849
850 case window_set_clipboard:
851 put_text_on_clipboard(event.clipboard_data());
852 return true;
853
854 case mouse_exit_window: // Mouse left window.
855 update_mouse_target({});
856 break;
857
858 case mouse_up:
859 case mouse_drag:
860 case mouse_down:
861 case mouse_move:
862 event.mouse().hitbox = _widget->hitbox_test(event.mouse().position);
863 if (event == mouse_down or event == mouse_move) {
864 update_mouse_target(event.mouse().hitbox.widget_id, event.mouse().position);
865 }
866 if (event == mouse_down) {
867 update_keyboard_target(event.mouse().hitbox.widget_id, keyboard_focus_group::all);
868 }
869 break;
870
871 default:;
872 }
873
874 // Translate keyboard events, using the keybindings.
875 auto events = std::vector<gui_event>{event};
876 if (event.type() == keyboard_down) {
877 for (auto& e : translate_keyboard_event(event)) {
878 events.push_back(e);
879 }
880 }
881
882 for (auto& event_ : events) {
883 if (event_.type() == gui_event_type::text_edit_paste) {
884 // The text-edit-paste operation was generated by keyboard bindings,
885 // it needs the actual text to be pasted added.
886 if (auto optional_text = get_text_from_clipboard()) {
887 event_.clipboard_data() = *optional_text;
888 }
889 }
890 }
891
892 // Send the event to the correct widget.
893 auto const handled = send_events_to_widget(
894 events.front().variant() == gui_event_variant::mouse ? _mouse_target_id : _keyboard_target_id, events);
895
896 // Intercept the keyboard generated escape.
897 // A keyboard generated escape should always remove keyboard focus.
898 // The update_keyboard_target() function will send gui_keyboard_exit and a
899 // potential duplicate gui_cancel messages to all widgets that need it.
900 for (auto const event_ : events) {
901 if (event_ == gui_cancel) {
902 update_keyboard_target({}, keyboard_focus_group::all);
903 }
904 }
905
906 return handled;
907 }
908
909private:
910 constexpr static UINT_PTR move_and_resize_timer_id = 2;
911 constexpr static std::chrono::nanoseconds _animation_duration = std::chrono::milliseconds(150);
912
913 inline static bool _first_window = true;
914 inline static const wchar_t *win32WindowClassName = nullptr;
915 inline static WNDCLASSW win32WindowClass = {};
916 inline static bool win32WindowClassIsRegistered = false;
917 inline static bool firstWindowHasBeenOpened = false;
918
921 label _title;
922
926
927 box_constraints _widget_constraints = {};
928
929 std::atomic<aarectangle> _redraw_rectangle = aarectangle{};
930 std::atomic<bool> _relayout = false;
931 std::atomic<bool> _reconstrain = false;
932 std::atomic<bool> _resize = false;
933
936 gui_window_size _size_state = gui_window_size::normal;
937
940 aarectangle _restore_rectangle;
941
949 utc_nanoseconds last_forced_redraw = {};
950
955 widget_id _mouse_target_id;
956
960 widget_id _keyboard_target_id;
961
962 TRACKMOUSEEVENT track_mouse_leave_event_parameters;
963 bool tracking_mouse_leave_event = false;
964 char32_t high_surrogate = 0;
965 gui_event mouse_button_event;
966 utc_nanoseconds multi_click_time_point;
967 point2 multi_click_position;
968 uint8_t multi_click_count;
969
970 bool keymenu_pressed = false;
971
972 callback<void()> _setting_change_cbt;
973 callback<void(std::string)> _selected_theme_cbt;
974 callback<void(utc_nanoseconds)> _render_cbt;
975
984 bool send_events_to_widget(widget_id target_id, std::vector<gui_event> const& events) noexcept
985 {
986 if (target_id == 0) {
987 // If there was no target, send the event to the window's widget.
988 target_id = _widget->id;
989 }
990
991 auto target_widget = get_if(_widget.get(), target_id, false);
992 while (target_widget) {
993 // Each widget will try to handle the first event it can.
994 for (auto const& event : events) {
995 if (target_widget->handle_event(target_widget->layout().from_window * event)) {
996 return true;
997 }
998 }
999
1000 // Forward the events to the parent of the target.
1001 target_widget = target_widget->parent;
1002 }
1003
1004 return false;
1005 }
1006
1007 void setOSWindowRectangleFromRECT(RECT new_rectangle) noexcept
1008 {
1009 hi_axiom(loop::main().on_thread());
1010
1011 // Convert bottom to y-axis up coordinate system.
1012 auto const inv_bottom = os_settings::primary_monitor_rectangle().height() - new_rectangle.bottom;
1013
1014 auto const new_screen_rectangle = aarectangle{
1015 narrow_cast<float>(new_rectangle.left),
1016 narrow_cast<float>(inv_bottom),
1017 narrow_cast<float>(new_rectangle.right - new_rectangle.left),
1018 narrow_cast<float>(new_rectangle.bottom - new_rectangle.top)};
1019
1020 if (rectangle.size() != new_screen_rectangle.size()) {
1021 ++global_counter<"gui_window:os-resize:relayout">;
1022 this->process_event({gui_event_type::window_relayout});
1023 }
1024
1025 rectangle = new_screen_rectangle;
1026 }
1027
1028 [[nodiscard]] keyboard_state get_keyboard_state() noexcept
1029 {
1030 auto r = keyboard_state::idle;
1031
1032 if (GetKeyState(VK_CAPITAL) != 0) {
1033 r |= keyboard_state::caps_lock;
1034 }
1035 if (GetKeyState(VK_NUMLOCK) != 0) {
1036 r |= keyboard_state::num_lock;
1037 }
1038 if (GetKeyState(VK_SCROLL) != 0) {
1039 r |= keyboard_state::scroll_lock;
1040 }
1041 return r;
1042 }
1043
1044 [[nodiscard]] keyboard_modifiers get_keyboard_modifiers() noexcept
1045 {
1046 // Documentation of GetAsyncKeyState() says that the held key is in the most-significant-bit.
1047 // Make sure it is signed, so that we can do a less-than 0 check. It looks like this function
1048 // was designed to be used this way.
1049 static_assert(std::is_signed_v<decltype(GetAsyncKeyState(VK_SHIFT))>);
1050
1051 auto r = keyboard_modifiers::none;
1052
1053 if (GetAsyncKeyState(VK_SHIFT) < 0) {
1054 r |= keyboard_modifiers::shift;
1055 }
1056 if (GetAsyncKeyState(VK_CONTROL) < 0) {
1057 r |= keyboard_modifiers::control;
1058 }
1059 if (GetAsyncKeyState(VK_MENU) < 0) {
1060 r |= keyboard_modifiers::alt;
1061 }
1062 if (GetAsyncKeyState(VK_LWIN) < 0 or GetAsyncKeyState(VK_RWIN) < 0) {
1063 r |= keyboard_modifiers::super;
1064 }
1065
1066 return r;
1067 }
1068
1069 [[nodiscard]] char32_t handle_suragates(char32_t c) noexcept
1070 {
1071 hi_axiom(loop::main().on_thread());
1072
1073 if (c >= 0xd800 && c <= 0xdbff) {
1074 high_surrogate = ((c - 0xd800) << 10) + 0x10000;
1075 return 0;
1076
1077 } else if (c >= 0xdc00 && c <= 0xdfff) {
1078 c = high_surrogate ? high_surrogate | (c - 0xdc00) : 0xfffd;
1079 }
1080 high_surrogate = 0;
1081 return c;
1082 }
1083
1084 [[nodiscard]] gui_event create_mouse_event(unsigned int uMsg, uint64_t wParam, int64_t lParam) noexcept
1085 {
1086 hi_axiom(loop::main().on_thread());
1087
1088 auto r = gui_event{gui_event_type::mouse_move};
1089 r.keyboard_modifiers = get_keyboard_modifiers();
1090 r.keyboard_state = get_keyboard_state();
1091
1092 auto const x = narrow_cast<float>(GET_X_LPARAM(lParam));
1093 auto const y = narrow_cast<float>(GET_Y_LPARAM(lParam));
1094
1095 // Convert to y-axis up coordinate system, y is in window-local.
1096 auto const inv_y = rectangle.height() - y;
1097
1098 // On Window 7 up to and including Window10, the I-beam cursor hot-spot is 2 pixels to the left
1099 // of the vertical bar. But most applications do not fix this problem.
1100 r.mouse().position = point2{x, inv_y};
1101 r.mouse().wheel_delta = {};
1102 if (uMsg == WM_MOUSEWHEEL) {
1103 r.mouse().wheel_delta.y() = GET_WHEEL_DELTA_WPARAM(wParam) * 10.0f / WHEEL_DELTA;
1104 } else if (uMsg == WM_MOUSEHWHEEL) {
1105 r.mouse().wheel_delta.x() = GET_WHEEL_DELTA_WPARAM(wParam) * 10.0f / WHEEL_DELTA;
1106 }
1107
1108 // Track which buttons are down, in case the application wants to track multiple buttons being pressed down.
1109 r.mouse().down.left_button = (GET_KEYSTATE_WPARAM(wParam) & MK_LBUTTON) > 0;
1110 r.mouse().down.middle_button = (GET_KEYSTATE_WPARAM(wParam) & MK_MBUTTON) > 0;
1111 r.mouse().down.right_button = (GET_KEYSTATE_WPARAM(wParam) & MK_RBUTTON) > 0;
1112 r.mouse().down.x1_button = (GET_KEYSTATE_WPARAM(wParam) & MK_XBUTTON1) > 0;
1113 r.mouse().down.x2_button = (GET_KEYSTATE_WPARAM(wParam) & MK_XBUTTON2) > 0;
1114
1115 // Check which buttons caused the mouse event.
1116 switch (uMsg) {
1117 case WM_LBUTTONUP:
1118 case WM_LBUTTONDOWN:
1119 case WM_LBUTTONDBLCLK:
1120 r.mouse().cause.left_button = true;
1121 break;
1122 case WM_RBUTTONUP:
1123 case WM_RBUTTONDOWN:
1124 case WM_RBUTTONDBLCLK:
1125 r.mouse().cause.right_button = true;
1126 break;
1127 case WM_MBUTTONUP:
1128 case WM_MBUTTONDOWN:
1129 case WM_MBUTTONDBLCLK:
1130 r.mouse().cause.middle_button = true;
1131 break;
1132 case WM_XBUTTONUP:
1133 case WM_XBUTTONDOWN:
1134 case WM_XBUTTONDBLCLK:
1135 r.mouse().cause.x1_button = (GET_XBUTTON_WPARAM(wParam) & XBUTTON1) > 0;
1136 r.mouse().cause.x2_button = (GET_XBUTTON_WPARAM(wParam) & XBUTTON2) > 0;
1137 break;
1138 case WM_MOUSEMOVE:
1139 if (mouse_button_event == gui_event_type::mouse_down) {
1140 r.mouse().cause = mouse_button_event.mouse().cause;
1141 }
1142 break;
1143 case WM_MOUSEWHEEL:
1144 case WM_MOUSEHWHEEL:
1145 case WM_MOUSELEAVE:
1146 break;
1147 default:
1148 hi_no_default();
1149 }
1150
1151 auto const a_button_is_pressed = r.mouse().down.left_button or r.mouse().down.middle_button or r.mouse().down.right_button or
1152 r.mouse().down.x1_button or r.mouse().down.x2_button;
1153
1154 switch (uMsg) {
1155 case WM_LBUTTONUP:
1156 case WM_MBUTTONUP:
1157 case WM_RBUTTONUP:
1158 case WM_XBUTTONUP:
1159 r.set_type(gui_event_type::mouse_up);
1160 if (mouse_button_event) {
1161 r.mouse().down_position = mouse_button_event.mouse().down_position;
1162 }
1163 r.mouse().click_count = 0;
1164
1165 if (!a_button_is_pressed) {
1166 ReleaseCapture();
1167 }
1168 break;
1169
1170 case WM_LBUTTONDBLCLK:
1171 case WM_MBUTTONDBLCLK:
1172 case WM_RBUTTONDBLCLK:
1173 case WM_XBUTTONDBLCLK:
1174 case WM_LBUTTONDOWN:
1175 case WM_MBUTTONDOWN:
1176 case WM_RBUTTONDOWN:
1177 case WM_XBUTTONDOWN:
1178 {
1179 auto const within_double_click_time = r.time_point - multi_click_time_point < os_settings::double_click_interval();
1180 auto const double_click_distance =
1181 std::sqrt(narrow_cast<float>(squared_hypot(r.mouse().position - multi_click_position)));
1182 auto const within_double_click_distance = double_click_distance < os_settings::double_click_distance();
1183
1184 multi_click_count = within_double_click_time and within_double_click_distance ? multi_click_count + 1 : 1;
1185 multi_click_time_point = r.time_point;
1186 multi_click_position = r.mouse().position;
1187
1188 r.set_type(gui_event_type::mouse_down);
1189 r.mouse().down_position = r.mouse().position;
1190 r.mouse().click_count = multi_click_count;
1191
1192 // Track draging past the window borders.
1193 hi_assert_not_null(win32Window);
1194 SetCapture(win32Window);
1195 }
1196 break;
1197
1198 case WM_MOUSEWHEEL:
1199 case WM_MOUSEHWHEEL:
1200 r.set_type(gui_event_type::mouse_wheel);
1201 break;
1202
1203 case WM_MOUSEMOVE:
1204 {
1205 // XXX Make sure the mouse is moved enough for this to cause a drag event.
1206 r.set_type(a_button_is_pressed ? gui_event_type::mouse_drag : gui_event_type::mouse_move);
1207 if (mouse_button_event) {
1208 r.mouse().down_position = mouse_button_event.mouse().down_position;
1209 r.mouse().click_count = mouse_button_event.mouse().click_count;
1210 }
1211 }
1212 break;
1213
1214 case WM_MOUSELEAVE:
1215 r.set_type(gui_event_type::mouse_exit_window);
1216 if (mouse_button_event) {
1217 r.mouse().down_position = mouse_button_event.mouse().down_position;
1218 }
1219 r.mouse().click_count = 0;
1220
1221 // After this event we need to ask win32 to track the mouse again.
1222 tracking_mouse_leave_event = false;
1223
1224 // Force current_mouse_cursor to None so that the Window is in a fresh
1225 // state when the mouse reenters it.
1226 current_mouse_cursor = mouse_cursor::None;
1227 break;
1228
1229 default:
1230 hi_no_default();
1231 }
1232
1233 // Make sure we start tracking mouse events when the mouse has entered the window again.
1234 // So that once the mouse leaves the window we receive a WM_MOUSELEAVE event.
1235 if (not tracking_mouse_leave_event and uMsg != WM_MOUSELEAVE) {
1236 auto *track_mouse_leave_event_parameters_p = &track_mouse_leave_event_parameters;
1237 if (not TrackMouseEvent(track_mouse_leave_event_parameters_p)) {
1238 hi_log_error("Could not track leave event '{}'", get_last_error_message());
1239 }
1240 tracking_mouse_leave_event = true;
1241 }
1242
1243 // Remember the last time a button was pressed or released, so that we can convert
1244 // a move into a drag event.
1245 if (r == gui_event_type::mouse_down or r == gui_event_type::mouse_up or r == gui_event_type::mouse_exit_window) {
1246 mouse_button_event = r;
1247 }
1248
1249 return r;
1250 }
1251
1252 void create_window(extent2 new_size)
1253 {
1254 // This function should be called during init(), and therefor should not have a lock on the window.
1255 hi_assert(loop::main().on_thread());
1256
1257 createWindowClass();
1258
1259 auto u16title = to_wstring(std::format("{}", _title));
1260
1261 hi_log_info("Create window of size {} with title '{}'", new_size, _title);
1262
1263 // Recommended to set the dpi-awareness before opening any window.
1264 SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
1265
1266 // We are opening a popup window with a caption bar to cause drop-shadow to appear around
1267 // the window.
1268 win32Window = CreateWindowExW(
1269 0, // Optional window styles.
1270 win32WindowClassName, // Window class
1271 u16title.data(), // Window text
1272 WS_OVERLAPPEDWINDOW, // Window style
1273 // Size and position
1274 500,
1275 500,
1276 round_cast<int>(new_size.width()),
1277 round_cast<int>(new_size.height()),
1278
1279 NULL, // Parent window
1280 NULL, // Menu
1281 reinterpret_cast<HINSTANCE>(crt_application_instance), // Instance handle
1282 this);
1283 if (win32Window == nullptr) {
1284 hi_log_fatal("Could not open a win32 window: {}", get_last_error_message());
1285 }
1286
1287 // Now we extend the drawable area over the titlebar and and border, excluding the drop shadow.
1288 // At least one value needs to be postive for the drop-shadow to be rendered.
1289 MARGINS m{0, 0, 0, 1};
1290 DwmExtendFrameIntoClientArea(win32Window, &m);
1291
1292 // Force WM_NCCALCSIZE to be send to the window.
1293 SetWindowPos(
1294 win32Window, nullptr, 0, 0, 0, 0, SWP_NOZORDER | SWP_NOOWNERZORDER | SWP_NOMOVE | SWP_NOSIZE | SWP_FRAMECHANGED);
1295
1296 if (!firstWindowHasBeenOpened) {
1297 auto const win32_window_ = win32Window;
1298 switch (gui_window_size::normal) {
1299 case gui_window_size::normal:
1300 ShowWindow(win32_window_, SW_SHOWNORMAL);
1301 break;
1302 case gui_window_size::minimized:
1303 ShowWindow(win32_window_, SW_SHOWMINIMIZED);
1304 break;
1305 case gui_window_size::maximized:
1306 ShowWindow(win32_window_, SW_SHOWMAXIMIZED);
1307 break;
1308 default:
1309 hi_no_default();
1310 }
1311 firstWindowHasBeenOpened = true;
1312 }
1313
1314 track_mouse_leave_event_parameters.cbSize = sizeof(track_mouse_leave_event_parameters);
1315 track_mouse_leave_event_parameters.dwFlags = TME_LEAVE;
1316 track_mouse_leave_event_parameters.hwndTrack = win32Window;
1317 track_mouse_leave_event_parameters.dwHoverTime = HOVER_DEFAULT;
1318
1319 ShowWindow(win32Window, SW_SHOW);
1320
1321 auto ppi_ = GetDpiForWindow(win32Window);
1322 if (ppi_ == 0) {
1323 throw gui_error("Could not retrieve dpi for window.");
1324 }
1325 pixel_density = {pixels_per_inch(ppi_), os_settings::device_type()};
1326
1327 surface = make_unique_gfx_surface(crt_application_instance, win32Window);
1328 }
1329
1330 int windowProc(unsigned int uMsg, uint64_t wParam, int64_t lParam) noexcept
1331 {
1332 using namespace std::chrono_literals;
1333
1334 gui_event mouse_event;
1335 auto const current_time = std::chrono::utc_clock::now();
1336
1337 switch (uMsg) {
1338 case WM_CLOSE:
1339 // WM_DESTROY is handled inside `_windowProc` since it has to deal with lifetime of `this`.
1340 break;
1341
1342 case WM_DESTROY:
1343 // WM_DESTROY is handled inside `_windowProc` since it has to deal with lifetime of `this`.
1344 break;
1345
1346 case WM_CREATE:
1347 {
1348 auto const createstruct_ptr = std::launder(std::bit_cast<CREATESTRUCT *>(lParam));
1349 RECT new_rectangle;
1350 new_rectangle.left = createstruct_ptr->x;
1351 new_rectangle.top = createstruct_ptr->y;
1352 new_rectangle.right = createstruct_ptr->x + createstruct_ptr->cx;
1353 new_rectangle.bottom = createstruct_ptr->y + createstruct_ptr->cy;
1354 setOSWindowRectangleFromRECT(new_rectangle);
1355 }
1356 break;
1357
1358 case WM_ERASEBKGND:
1359 return 1;
1360
1361 case WM_PAINT:
1362 {
1363 auto const height = [this]() {
1364 hi_axiom(loop::main().on_thread());
1365 return rectangle.height();
1366 }();
1367
1368 PAINTSTRUCT ps;
1369 BeginPaint(win32Window, &ps);
1370
1371 auto const update_rectangle = aarectangle{
1372 narrow_cast<float>(ps.rcPaint.left),
1373 narrow_cast<float>(height - ps.rcPaint.bottom),
1374 narrow_cast<float>(ps.rcPaint.right - ps.rcPaint.left),
1375 narrow_cast<float>(ps.rcPaint.bottom - ps.rcPaint.top)};
1376
1377 {
1378 hi_axiom(loop::main().on_thread());
1379 this->process_event({gui_event_type::window_redraw, update_rectangle});
1380 }
1381
1382 EndPaint(win32Window, &ps);
1383 }
1384 break;
1385
1386 case WM_NCPAINT:
1387 hi_axiom(loop::main().on_thread());
1388 this->process_event({gui_event_type::window_redraw, aarectangle{rectangle.size()}});
1389 break;
1390
1391 case WM_SIZE:
1392 // This is called when the operating system is changing the size of the window.
1393 // However we do not support maximizing by the OS.
1394 hi_axiom(loop::main().on_thread());
1395 switch (wParam) {
1396 case SIZE_MAXIMIZED:
1397 ShowWindow(win32Window, SW_RESTORE);
1398 set_size_state(gui_window_size::maximized);
1399 break;
1400 case SIZE_MINIMIZED:
1401 _size_state = gui_window_size::minimized;
1402 break;
1403 case SIZE_RESTORED:
1404 _size_state = gui_window_size::normal;
1405 break;
1406 default:
1407 break;
1408 }
1409 break;
1410
1411 case WM_TIMER:
1412 if (last_forced_redraw + 16.7ms < current_time) {
1413 // During sizing the event loop is blocked.
1414 // Render at about 60fps.
1415 loop::main().resume_once();
1416 last_forced_redraw = current_time;
1417 }
1418 break;
1419
1420 case WM_SIZING:
1421 {
1422 auto const& rect_ptr = *std::launder(std::bit_cast<RECT *>(lParam));
1423 if (rect_ptr.right < rect_ptr.left or rect_ptr.bottom < rect_ptr.top) {
1424 hi_log_error(
1425 "Invalid RECT received on WM_SIZING: left={}, right={}, bottom={}, top={}",
1426 rect_ptr.left,
1427 rect_ptr.right,
1428 rect_ptr.bottom,
1429 rect_ptr.top);
1430
1431 } else {
1432 setOSWindowRectangleFromRECT(rect_ptr);
1433 }
1434 }
1435 break;
1436
1437 case WM_MOVING:
1438 {
1439 auto const& rect_ptr = *std::launder(std::bit_cast<RECT *>(lParam));
1440 if (rect_ptr.right < rect_ptr.left or rect_ptr.bottom < rect_ptr.top) {
1441 hi_log_error(
1442 "Invalid RECT received on WM_MOVING: left={}, right={}, bottom={}, top={}",
1443 rect_ptr.left,
1444 rect_ptr.right,
1445 rect_ptr.bottom,
1446 rect_ptr.top);
1447
1448 } else {
1449 setOSWindowRectangleFromRECT(rect_ptr);
1450 }
1451 }
1452 break;
1453
1454 case WM_WINDOWPOSCHANGED:
1455 {
1456 auto const windowpos_ptr = std::launder(std::bit_cast<WINDOWPOS *>(lParam));
1457 RECT new_rectangle;
1458 new_rectangle.left = windowpos_ptr->x;
1459 new_rectangle.top = windowpos_ptr->y;
1460 new_rectangle.right = windowpos_ptr->x + windowpos_ptr->cx;
1461 new_rectangle.bottom = windowpos_ptr->y + windowpos_ptr->cy;
1462 setOSWindowRectangleFromRECT(new_rectangle);
1463 }
1464 break;
1465
1466 case WM_ENTERSIZEMOVE:
1467 hi_axiom(loop::main().on_thread());
1468 if (SetTimer(win32Window, move_and_resize_timer_id, 16, NULL) != move_and_resize_timer_id) {
1469 hi_log_error("Could not set timer before move/resize. {}", get_last_error_message());
1470 }
1471 resizing = true;
1472 break;
1473
1474 case WM_EXITSIZEMOVE:
1475 hi_axiom(loop::main().on_thread());
1476 if (not KillTimer(win32Window, move_and_resize_timer_id)) {
1477 hi_log_error("Could not kill timer after move/resize. {}", get_last_error_message());
1478 }
1479 resizing = false;
1480 // After a manual move of the window, it is clear that the window is in normal mode.
1481 _restore_rectangle = rectangle;
1482 _size_state = gui_window_size::normal;
1483 this->process_event({gui_event_type::window_redraw, aarectangle{rectangle.size()}});
1484 break;
1485
1486 case WM_ACTIVATE:
1487 hi_axiom(loop::main().on_thread());
1488 switch (wParam) {
1489 case 1: // WA_ACTIVE
1490 case 2: // WA_CLICKACTIVE
1491 this->process_event({gui_event_type::window_activate});
1492 break;
1493 case 0: // WA_INACTIVE
1494 this->process_event({gui_event_type::window_deactivate});
1495 break;
1496 default:
1497 hi_log_error("Unknown WM_ACTIVE value.");
1498 }
1499 ++global_counter<"gui_window:WM_ACTIVATE:constrain">;
1500 this->process_event({gui_event_type::window_reconstrain});
1501 break;
1502
1503 case WM_GETMINMAXINFO:
1504 {
1505 hi_axiom(loop::main().on_thread());
1506 auto const minmaxinfo = std::launder(std::bit_cast<MINMAXINFO *>(lParam));
1507 minmaxinfo->ptMaxSize.x = round_cast<LONG>(_widget_constraints.maximum.width());
1508 minmaxinfo->ptMaxSize.y = round_cast<LONG>(_widget_constraints.maximum.height());
1509 minmaxinfo->ptMinTrackSize.x = round_cast<LONG>(_widget_constraints.minimum.width());
1510 minmaxinfo->ptMinTrackSize.y = round_cast<LONG>(_widget_constraints.minimum.height());
1511 minmaxinfo->ptMaxTrackSize.x = round_cast<LONG>(_widget_constraints.maximum.width());
1512 minmaxinfo->ptMaxTrackSize.y = round_cast<LONG>(_widget_constraints.maximum.height());
1513 }
1514 break;
1515
1516 case WM_UNICHAR:
1517 if (auto c = char_cast<char32_t>(wParam); c == UNICODE_NOCHAR) {
1518 // Tell the 3rd party keyboard handler application that we support WM_UNICHAR.
1519 return 1;
1520
1521 } else if (auto const gc = ucd_get_general_category(c); not is_C(gc) and not is_M(gc)) {
1522 // Only pass code-points that are non-control and non-mark.
1523 process_event(gui_event::keyboard_grapheme(grapheme{c}));
1524 }
1525 break;
1526
1527 case WM_DEADCHAR:
1528 if (auto c = handle_suragates(char_cast<char32_t>(wParam))) {
1529 if (auto const gc = ucd_get_general_category(c); not is_C(gc) and not is_M(gc)) {
1530 // Only pass code-points that are non-control and non-mark.
1531 process_event(gui_event::keyboard_partial_grapheme(grapheme{c}));
1532 }
1533 }
1534 break;
1535
1536 case WM_CHAR:
1537 if (auto c = handle_suragates(char_cast<char32_t>(wParam))) {
1538 if (auto const gc = ucd_get_general_category(c); not is_C(gc) and not is_M(gc)) {
1539 // Only pass code-points that are non-control and non-mark.
1540 process_event(gui_event::keyboard_grapheme(grapheme{c}));
1541 }
1542 }
1543 break;
1544
1545 case WM_SYSCOMMAND:
1546 if (wParam == SC_KEYMENU) {
1547 keymenu_pressed = true;
1548 process_event(gui_event{gui_event_type::keyboard_down, keyboard_virtual_key::menu});
1549 return 0;
1550 }
1551 break;
1552
1553 case WM_KEYDOWN:
1554 case WM_KEYUP:
1555 {
1556 auto const extended = (narrow_cast<uint32_t>(lParam) & 0x01000000) != 0;
1557 auto const key_code = narrow_cast<int>(wParam);
1558 auto const key_modifiers = get_keyboard_modifiers();
1559 auto virtual_key = to_keyboard_virtual_key(key_code, extended, key_modifiers);
1560
1561 if (std::exchange(keymenu_pressed, false) and uMsg == WM_KEYDOWN and virtual_key == keyboard_virtual_key::space) {
1562 // On windows, Alt followed by Space opens the menu of the window, which is called the system menu.
1563 virtual_key = keyboard_virtual_key::sysmenu;
1564 }
1565
1566 if (virtual_key != keyboard_virtual_key::nul) {
1567 auto const key_state = get_keyboard_state();
1568 auto const event_type = uMsg == WM_KEYDOWN ? gui_event_type::keyboard_down : gui_event_type::keyboard_up;
1569 process_event(gui_event{event_type, virtual_key, key_modifiers, key_state});
1570 }
1571 }
1572 break;
1573
1574 case WM_LBUTTONDOWN:
1575 case WM_MBUTTONDOWN:
1576 case WM_RBUTTONDOWN:
1577 case WM_XBUTTONDOWN:
1578 case WM_LBUTTONUP:
1579 case WM_MBUTTONUP:
1580 case WM_RBUTTONUP:
1581 case WM_XBUTTONUP:
1582 case WM_LBUTTONDBLCLK:
1583 case WM_MBUTTONDBLCLK:
1584 case WM_RBUTTONDBLCLK:
1585 case WM_XBUTTONDBLCLK:
1586 case WM_MOUSEWHEEL:
1587 case WM_MOUSEHWHEEL:
1588 case WM_MOUSEMOVE:
1589 case WM_MOUSELEAVE:
1590 keymenu_pressed = false;
1591 process_event(create_mouse_event(uMsg, wParam, lParam));
1592 break;
1593
1594 case WM_NCCALCSIZE:
1595 if (wParam == TRUE) {
1596 // When wParam is TRUE, simply returning 0 without processing the NCCALCSIZE_PARAMS rectangles
1597 // will cause the client area to resize to the size of the window, including the window frame.
1598 // This will remove the window frame and caption items from your window, leaving only the client area displayed.
1599 //
1600 // Starting with Windows Vista, removing the standard frame by simply
1601 // returning 0 when the wParam is TRUE does not affect frames that are
1602 // extended into the client area using the DwmExtendFrameIntoClientArea function.
1603 // Only the standard frame will be removed.
1604 return 0;
1605 }
1606
1607 break;
1608
1609 case WM_NCHITTEST:
1610 {
1611 hi_axiom(loop::main().on_thread());
1612
1613 auto const x = narrow_cast<float>(GET_X_LPARAM(lParam));
1614 auto const y = narrow_cast<float>(GET_Y_LPARAM(lParam));
1615
1616 // Convert to y-axis up coordinate system.
1617 auto const inv_y = os_settings::primary_monitor_rectangle().height() - y;
1618
1619 auto const hitbox_type = _widget->hitbox_test(screen_to_window() * point2{x, inv_y}).type;
1620
1621 switch (hitbox_type) {
1622 case hitbox_type::bottom_resize_border:
1623 set_cursor(mouse_cursor::None);
1624 return HTBOTTOM;
1625 case hitbox_type::top_resize_border:
1626 set_cursor(mouse_cursor::None);
1627 return HTTOP;
1628 case hitbox_type::left_resize_border:
1629 set_cursor(mouse_cursor::None);
1630 return HTLEFT;
1631 case hitbox_type::right_resize_border:
1632 set_cursor(mouse_cursor::None);
1633 return HTRIGHT;
1634 case hitbox_type::bottom_left_resize_corner:
1635 set_cursor(mouse_cursor::None);
1636 return HTBOTTOMLEFT;
1637 case hitbox_type::bottom_right_resize_corner:
1638 set_cursor(mouse_cursor::None);
1639 return HTBOTTOMRIGHT;
1640 case hitbox_type::top_left_resize_corner:
1641 set_cursor(mouse_cursor::None);
1642 return HTTOPLEFT;
1643 case hitbox_type::top_right_resize_corner:
1644 set_cursor(mouse_cursor::None);
1645 return HTTOPRIGHT;
1646 case hitbox_type::application_icon:
1647 set_cursor(mouse_cursor::None);
1648 return HTSYSMENU;
1649 case hitbox_type::move_area:
1650 set_cursor(mouse_cursor::None);
1651 return HTCAPTION;
1652 case hitbox_type::text_edit:
1653 set_cursor(mouse_cursor::TextEdit);
1654 return HTCLIENT;
1655 case hitbox_type::button:
1656 set_cursor(mouse_cursor::Button);
1657 return HTCLIENT;
1658 case hitbox_type::scroll_bar:
1659 set_cursor(mouse_cursor::Default);
1660 return HTCLIENT;
1661 case hitbox_type::_default:
1662 set_cursor(mouse_cursor::Default);
1663 return HTCLIENT;
1664 case hitbox_type::outside:
1665 set_cursor(mouse_cursor::None);
1666 return HTCLIENT;
1667 default:
1668 hi_no_default();
1669 }
1670 }
1671 break;
1672
1673 case WM_SETTINGCHANGE:
1674 hi_axiom(loop::main().on_thread());
1675 os_settings::gather();
1676 break;
1677
1678 case WM_DPICHANGED:
1679 {
1680 hi_axiom(loop::main().on_thread());
1681 // x-axis dpi value.
1682 pixel_density = {pixels_per_inch(LOWORD(wParam)), os_settings::device_type()};
1683
1684 // Use the recommended rectangle to resize and reposition the window
1685 auto const new_rectangle = std::launder(reinterpret_cast<RECT *>(lParam));
1686 SetWindowPos(
1687 win32Window,
1688 NULL,
1689 new_rectangle->left,
1690 new_rectangle->top,
1691 new_rectangle->right - new_rectangle->left,
1692 new_rectangle->bottom - new_rectangle->top,
1693 SWP_NOZORDER | SWP_NOACTIVATE);
1694 ++global_counter<"gui_window:WM_DPICHANGED:constrain">;
1695 this->process_event({gui_event_type::window_reconstrain});
1696
1697 // XXX #667 use mp-units formatting.
1698 hi_log_info("DPI has changed to {} ppi", pixel_density.ppi.in(pixels_per_inch));
1699 }
1700 break;
1701
1702 default:
1703 break;
1704 }
1705
1706 // Let DefWindowProc() handle it.
1707 return -1;
1708 }
1709
1713 static LRESULT CALLBACK _WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) noexcept
1714 {
1715 if (uMsg == WM_CREATE && lParam) {
1716 auto const createData = std::launder(std::bit_cast<CREATESTRUCT *>(lParam));
1717
1718 SetLastError(0);
1719 auto r = SetWindowLongPtrW(hwnd, GWLP_USERDATA, std::bit_cast<LONG_PTR>(createData->lpCreateParams));
1720 if (r != 0 || GetLastError() != 0) {
1721 hi_log_fatal("Could not set GWLP_USERDATA on window. '{}'", get_last_error_message());
1722 }
1723 }
1724
1725 // It is assumed that GWLP_USERDATA is zero when the window is created. Because messages to
1726 // this window are send before WM_CREATE and there is no way to figure out to which actual window
1727 // these messages belong.
1728 auto window_userdata = GetWindowLongPtrW(hwnd, GWLP_USERDATA);
1729 if (window_userdata == 0) {
1730 return DefWindowProc(hwnd, uMsg, wParam, lParam);
1731 }
1732
1733 auto& window = *std::launder(std::bit_cast<gui_window *>(window_userdata));
1734 hi_axiom(loop::main().on_thread());
1735
1736 // WM_CLOSE and WM_DESTROY will re-enter and run the destructor for `window`.
1737 // We can no longer call virtual functions on the `window` object.
1738 if (uMsg == WM_CLOSE) {
1739 // Listeners can close the window by calling the destructor on `window`.
1740 window.closing();
1741 return 0;
1742
1743 } else if (uMsg == WM_DESTROY) {
1744 // Remove the window now, before DefWindowProc, which could recursively
1745 // Reuse the window as it is being cleaned up.
1746 SetLastError(0);
1747 auto r = SetWindowLongPtrW(hwnd, GWLP_USERDATA, NULL);
1748 if (r == 0 || GetLastError() != 0) {
1749 hi_log_fatal("Could not set GWLP_USERDATA on window. '{}'", get_last_error_message());
1750 }
1751
1752 // Also remove the win32Window from the window, so that we don't get double DestroyWindow().
1753 window.win32Window = nullptr;
1754 return 0;
1755
1756 } else {
1757 if (auto result = window.windowProc(uMsg, wParam, lParam); result != -1) {
1758 return result;
1759 }
1760 return DefWindowProc(hwnd, uMsg, wParam, lParam);
1761 }
1762 }
1763
1764 static void createWindowClass()
1765 {
1766 if (!win32WindowClassIsRegistered) {
1767 // Register the window class.
1768 win32WindowClassName = L"HikoGUI Window Class";
1769
1770 std::memset(&win32WindowClass, 0, sizeof(WNDCLASSW));
1771 win32WindowClass.style = CS_DBLCLKS;
1772 win32WindowClass.lpfnWndProc = _WindowProc;
1773 win32WindowClass.hInstance = static_cast<HINSTANCE>(crt_application_instance);
1774 win32WindowClass.lpszClassName = win32WindowClassName;
1775 win32WindowClass.hCursor = nullptr;
1776 RegisterClassW(&win32WindowClass);
1777 }
1778 win32WindowClassIsRegistered = true;
1779 }
1780};
1781
1782} // namespace hi::inline v1
Rules for working with win32 headers.
Definition of GUI event types.
constexpr std::wstring to_wstring(std::u32string_view rhs) noexcept
Conversion from UTF-32 to wide-string (UTF-16/32).
Definition to_string.hpp:160
gui_event_type
GUI event type.
Definition gui_event_type.hpp:24
@ rectangle
The gui_event has rectangle data.
hi_export std::string get_last_error_message(uint32_t error_code)
Get the error message from an error code.
Definition exception_win32_impl.hpp:21
DOXYGEN BUG.
Definition algorithm_misc.hpp:20
constexpr gstring to_gstring(std::u32string_view rhs, unicode_normalize_config config=unicode_normalize_config::NFC()) noexcept
Convert a UTF-32 string-view to a grapheme-string.
Definition gstring.hpp:263
subpixel_orientation
The orientation of the RGB sub-pixels of and LCD/LED panel.
Definition subpixel_orientation.hpp:21
hi_export font & register_font_file(std::filesystem::path const &path)
Register a font.
Definition font_book.hpp:381
keyboard_focus_direction
The keyboard focus group used for finding a widget that will accept a particular focus.
Definition keyboard_focus_direction.hpp:15
keyboard_modifiers
Key modification keys pressed at the same time as another key.
Definition keyboard_modifiers.hpp:27
constexpr std::u32string unicode_normalize(std::u32string_view text, unicode_normalize_config config=unicode_normalize_config::NFC()) noexcept
Convert text to a Unicode composed normal form.
Definition unicode_normalization.hpp:308
os_handle crt_application_instance
The application instance identified by the operating system.
Definition crt_utils_intf.hpp:24
keyboard_focus_group
The keyboard focus group used for finding a widget that will accept a particular focus.
Definition keyboard_focus_group.hpp:17
A notifier which can be used to call a set of registered callbacks.
Definition notifier.hpp:26
A high-level geometric point Part of the high-level vec, point, mat and color types.
Definition point2.hpp:28
Definition gui_window_win32.hpp:25
void update_keyboard_target(widget_id start_widget, keyboard_focus_group group, keyboard_focus_direction direction) noexcept
Change the keyboard focus to the previous or next widget from the given widget.
Definition gui_window_win32.hpp:627
aarectangle fullscreen_rectangle() const noexcept
The rectangle of the screen where the window is currently located.
Definition gui_window_win32.hpp:446
void update_keyboard_target(keyboard_focus_group group, keyboard_focus_direction direction) noexcept
Change the keyboard focus to the given, previous or next widget.
Definition gui_window_win32.hpp:649
void set_window_size(extent2 new_extent)
Ask the operating system to set the size of this window.
Definition gui_window_win32.hpp:527
void put_text_on_clipboard(gstring_view text) const noexcept
Put text on the clipboard.
Definition gui_window_win32.hpp:720
void set_cursor(mouse_cursor cursor) noexcept
Set the mouse cursor icon.
Definition gui_window_win32.hpp:296
void open_system_menu()
Open the system menu of the window.
Definition gui_window_win32.hpp:505
void close_window()
Ask the operating system to close this window.
Definition gui_window_win32.hpp:338
void render(utc_nanoseconds display_time_point)
Update window.
Definition gui_window_win32.hpp:182
aarectangle rectangle
The current rectangle of the window relative to the screen.
Definition gui_window_win32.hpp:40
extent2 widget_size
The size of the widget.
Definition gui_window_win32.hpp:70
void set_size_state(gui_window_size state) noexcept
Set the size-state of the window.
Definition gui_window_win32.hpp:351
std::optional< gstring > get_text_from_clipboard() const noexcept
Get text from the clipboard.
Definition gui_window_win32.hpp:661
aarectangle workspace_rectangle() const noexcept
The rectangle of the workspace of the screen where the window is currently located.
Definition gui_window_win32.hpp:419
notifier< void()> closing
Notifier used when the window is closing.
Definition gui_window_win32.hpp:75
void update_keyboard_target(widget_id new_target_id, keyboard_focus_group group=keyboard_focus_group::normal) noexcept
Change the keyboard focus to the given widget.
Definition gui_window_win32.hpp:580
gui_window_size size_state() const noexcept
Get the size-state of the window.
Definition gui_window_win32.hpp:473
bool process_event(gui_event event) noexcept
Process the event.
Definition gui_window_win32.hpp:794
Definition theme.hpp:24
theme transform(hi::pixel_density new_pixel_density) const noexcept
Create a transformed copy of the theme.
Definition theme.hpp:139
Definition trace.hpp:43
Definition pixel_density.hpp:16
T get(T... args)
T memcpy(T... args)
T memset(T... args)
T min(T... args)
T move(T... args)
T push_back(T... args)
T reset(T... args)
T sqrt(T... args)
T to_wstring(T... args)
T what(T... args)