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