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