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