aboutsummaryrefslogtreecommitdiff
path: root/src/win/gui
diff options
context:
space:
mode:
Diffstat (limited to 'src/win/gui')
-rw-r--r--src/win/gui/CMakeLists.txt31
-rw-r--r--src/win/gui/Cursor.cpp51
-rw-r--r--src/win/gui/GodWindow.cpp63
-rw-r--r--src/win/gui/InputMethod.cpp278
-rw-r--r--src/win/gui/Keyboard.cpp74
-rw-r--r--src/win/gui/TimerManager.cpp100
-rw-r--r--src/win/gui/TimerManager.hpp61
-rw-r--r--src/win/gui/UiApplication.cpp118
-rw-r--r--src/win/gui/Window.cpp453
-rw-r--r--src/win/gui/WindowClass.cpp28
-rw-r--r--src/win/gui/WindowManager.cpp56
-rw-r--r--src/win/gui/WindowManager.hpp51
12 files changed, 1364 insertions, 0 deletions
diff --git a/src/win/gui/CMakeLists.txt b/src/win/gui/CMakeLists.txt
new file mode 100644
index 00000000..48bed00d
--- /dev/null
+++ b/src/win/gui/CMakeLists.txt
@@ -0,0 +1,31 @@
+set(CRU_WIN_GUI_INCLUDE_DIR ${CRU_INCLUDE_DIR}/cru/win/gui)
+
+add_library(cru_win_gui STATIC
+ TimerManager.hpp
+ WindowManager.hpp
+
+ Cursor.cpp
+ GodWindow.cpp
+ InputMethod.cpp
+ Keyboard.cpp
+ TimerManager.cpp
+ UiApplication.cpp
+ Window.cpp
+ WindowClass.cpp
+ WindowManager.cpp
+)
+target_sources(cru_win_gui PUBLIC
+ ${CRU_WIN_GUI_INCLUDE_DIR}/Cursor.hpp
+ ${CRU_WIN_GUI_INCLUDE_DIR}/Exception.hpp
+ ${CRU_WIN_GUI_INCLUDE_DIR}/Base.hpp
+ ${CRU_WIN_GUI_INCLUDE_DIR}/GodWindow.hpp
+ ${CRU_WIN_GUI_INCLUDE_DIR}/InputMethod.hpp
+ ${CRU_WIN_GUI_INCLUDE_DIR}/Keyboard.hpp
+ ${CRU_WIN_GUI_INCLUDE_DIR}/Resource.hpp
+ ${CRU_WIN_GUI_INCLUDE_DIR}/UiApplication.hpp
+ ${CRU_WIN_GUI_INCLUDE_DIR}/Window.hpp
+ ${CRU_WIN_GUI_INCLUDE_DIR}/WindowClass.hpp
+ ${CRU_WIN_GUI_INCLUDE_DIR}/WindowNativeMessageEventArgs.hpp
+)
+target_link_libraries(cru_win_gui PUBLIC imm32)
+target_link_libraries(cru_win_gui PUBLIC cru_win_graphics_direct cru_platform_gui)
diff --git a/src/win/gui/Cursor.cpp b/src/win/gui/Cursor.cpp
new file mode 100644
index 00000000..5f3086fa
--- /dev/null
+++ b/src/win/gui/Cursor.cpp
@@ -0,0 +1,51 @@
+#include "cru/win/gui/Cursor.hpp"
+
+#include "cru/common/Logger.hpp"
+#include "cru/win/gui/Exception.hpp"
+
+#include <stdexcept>
+
+namespace cru::platform::gui::win {
+WinCursor::WinCursor(HCURSOR handle, bool auto_destroy) {
+ handle_ = handle;
+ auto_destroy_ = auto_destroy;
+}
+
+WinCursor::~WinCursor() {
+ if (auto_destroy_) {
+ if (!::DestroyCursor(handle_)) {
+ // This is not a fetal error but might still need notice because it may
+ // cause leak.
+ log::TagWarn(log_tag, u"Failed to destroy a cursor. Last error code: {}",
+ ::GetLastError());
+ }
+ }
+}
+
+namespace {
+WinCursor* LoadWinCursor(const wchar_t* name) {
+ const auto handle = static_cast<HCURSOR>(::LoadImageW(
+ NULL, name, IMAGE_CURSOR, SM_CYCURSOR, SM_CYCURSOR, LR_SHARED));
+ if (handle == NULL) {
+ throw Win32Error(::GetLastError(), "Failed to load system cursor.");
+ }
+ return new WinCursor(handle, false);
+}
+} // namespace
+
+WinCursorManager::WinCursorManager()
+ : sys_arrow_(LoadWinCursor(IDC_ARROW)),
+ sys_hand_(LoadWinCursor(IDC_HAND)) {}
+
+std::shared_ptr<WinCursor> WinCursorManager::GetSystemWinCursor(
+ SystemCursorType type) {
+ switch (type) {
+ case SystemCursorType::Arrow:
+ return sys_arrow_;
+ case SystemCursorType::Hand:
+ return sys_hand_;
+ default:
+ throw std::runtime_error("Unknown system cursor value.");
+ }
+}
+} // namespace cru::platform::gui::win
diff --git a/src/win/gui/GodWindow.cpp b/src/win/gui/GodWindow.cpp
new file mode 100644
index 00000000..7bce83a3
--- /dev/null
+++ b/src/win/gui/GodWindow.cpp
@@ -0,0 +1,63 @@
+#include "cru/win/gui/GodWindow.hpp"
+
+#include "cru/common/Logger.hpp"
+#include "cru/win/gui/Exception.hpp"
+#include "cru/win/gui/UiApplication.hpp"
+#include "cru/win/gui/WindowClass.hpp"
+
+namespace cru::platform::gui::win {
+constexpr auto god_window_class_name = L"GodWindowClass";
+
+LRESULT CALLBACK GodWndProc(HWND hWnd, UINT uMsg, WPARAM wParam,
+ LPARAM lParam) {
+ const auto app = WinUiApplication::GetInstance();
+
+ if (app) {
+ LRESULT result;
+ auto god_window = app->GetGodWindow();
+ if (god_window != nullptr) {
+ const auto handled = god_window->HandleGodWindowMessage(
+ hWnd, uMsg, wParam, lParam, &result);
+ if (handled) return result;
+ }
+ }
+ return DefWindowProcW(hWnd, uMsg, wParam, lParam);
+}
+
+GodWindow::GodWindow(WinUiApplication* application) {
+ application_ = application;
+
+ const auto h_instance = application->GetInstanceHandle();
+
+ god_window_class_ = std::make_unique<WindowClass>(god_window_class_name,
+ GodWndProc, h_instance);
+
+ hwnd_ = CreateWindowEx(0, god_window_class_name, L"", 0, CW_USEDEFAULT,
+ CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
+ HWND_MESSAGE, nullptr, h_instance, nullptr);
+
+ if (hwnd_ == nullptr)
+ throw Win32Error(::GetLastError(), "Failed to create god window.");
+}
+
+GodWindow::~GodWindow() {
+ if (!::DestroyWindow(hwnd_)) {
+ // Although this could be "safely" ignore.
+ log::TagWarn(log_tag, u"Failed to destroy god window.");
+ }
+}
+
+bool GodWindow::HandleGodWindowMessage(HWND hwnd, UINT msg, WPARAM w_param,
+ LPARAM l_param, LRESULT* result) {
+ WindowNativeMessageEventArgs args(
+ WindowNativeMessage{hwnd, msg, w_param, l_param});
+ message_event_.Raise(args);
+
+ if (args.IsHandled()) {
+ *result = args.GetResult();
+ return true;
+ }
+
+ return false;
+}
+} // namespace cru::platform::gui::win
diff --git a/src/win/gui/InputMethod.cpp b/src/win/gui/InputMethod.cpp
new file mode 100644
index 00000000..d6f2146d
--- /dev/null
+++ b/src/win/gui/InputMethod.cpp
@@ -0,0 +1,278 @@
+#include "cru/win/gui/InputMethod.hpp"
+
+#include "cru/common/Logger.hpp"
+#include "cru/common/StringUtil.hpp"
+#include "cru/platform/Check.hpp"
+#include "cru/win/Exception.hpp"
+#include "cru/win/gui/Window.hpp"
+
+#include <vector>
+
+namespace cru::platform::gui::win {
+AutoHIMC::AutoHIMC(HWND hwnd) : hwnd_(hwnd) {
+ Expects(hwnd);
+ handle_ = ::ImmGetContext(hwnd);
+}
+
+AutoHIMC::AutoHIMC(AutoHIMC&& other)
+ : hwnd_(other.hwnd_), handle_(other.handle_) {
+ other.hwnd_ = nullptr;
+ other.handle_ = nullptr;
+}
+
+AutoHIMC& AutoHIMC::operator=(AutoHIMC&& other) {
+ if (this != &other) {
+ Object::operator=(std::move(other));
+ this->hwnd_ = other.hwnd_;
+ this->handle_ = other.handle_;
+ other.hwnd_ = nullptr;
+ other.handle_ = nullptr;
+ }
+ return *this;
+}
+
+AutoHIMC::~AutoHIMC() {
+ if (handle_) {
+ if (!::ImmReleaseContext(hwnd_, handle_))
+ log::TagWarn(log_tag, u"Failed to release HIMC.");
+ }
+}
+
+// copied from chromium
+namespace {
+// Determines whether or not the given attribute represents a target
+// (a.k.a. a selection).
+bool IsTargetAttribute(char attribute) {
+ return (attribute == ATTR_TARGET_CONVERTED ||
+ attribute == ATTR_TARGET_NOTCONVERTED);
+}
+// Helper function for ImeInput::GetCompositionInfo() method, to get the target
+// range that's selected by the user in the current composition string.
+void GetCompositionTargetRange(HIMC imm_context, int* target_start,
+ int* target_end) {
+ int attribute_size =
+ ::ImmGetCompositionString(imm_context, GCS_COMPATTR, NULL, 0);
+ if (attribute_size > 0) {
+ int start = 0;
+ int end = 0;
+ std::vector<char> attribute_data(attribute_size);
+ ::ImmGetCompositionString(imm_context, GCS_COMPATTR, attribute_data.data(),
+ attribute_size);
+ for (start = 0; start < attribute_size; ++start) {
+ if (IsTargetAttribute(attribute_data[start])) break;
+ }
+ for (end = start; end < attribute_size; ++end) {
+ if (!IsTargetAttribute(attribute_data[end])) break;
+ }
+ if (start == attribute_size) {
+ // This composition clause does not contain any target clauses,
+ // i.e. this clauses is an input clause.
+ // We treat the whole composition as a target clause.
+ start = 0;
+ end = attribute_size;
+ }
+ *target_start = start;
+ *target_end = end;
+ }
+}
+// Helper function for ImeInput::GetCompositionInfo() method, to get underlines
+// information of the current composition string.
+CompositionClauses GetCompositionClauses(HIMC imm_context, int target_start,
+ int target_end) {
+ CompositionClauses result;
+ int clause_size =
+ ::ImmGetCompositionString(imm_context, GCS_COMPCLAUSE, NULL, 0);
+ int clause_length = clause_size / sizeof(std::uint32_t);
+ if (clause_length) {
+ result.reserve(clause_length - 1);
+ std::vector<std::uint32_t> clause_data(clause_length);
+ ::ImmGetCompositionString(imm_context, GCS_COMPCLAUSE, clause_data.data(),
+ clause_size);
+ for (int i = 0; i < clause_length - 1; ++i) {
+ CompositionClause clause;
+ clause.start = clause_data[i];
+ clause.end = clause_data[i + 1];
+ clause.target = false;
+ // Use thick underline for the target clause.
+ if (clause.start >= target_start && clause.end <= target_end) {
+ clause.target = true;
+ }
+ result.push_back(clause);
+ }
+ }
+ return result;
+}
+
+std::u16string GetString(HIMC imm_context) {
+ LONG string_size =
+ ::ImmGetCompositionString(imm_context, GCS_COMPSTR, NULL, 0);
+ std::u16string result((string_size / sizeof(char16_t)), 0);
+ ::ImmGetCompositionString(imm_context, GCS_COMPSTR, result.data(),
+ string_size);
+ return result;
+}
+
+std::u16string GetResultString(HIMC imm_context) {
+ LONG string_size =
+ ::ImmGetCompositionString(imm_context, GCS_RESULTSTR, NULL, 0);
+ std::u16string result((string_size / sizeof(char16_t)), 0);
+ ::ImmGetCompositionString(imm_context, GCS_RESULTSTR, result.data(),
+ string_size);
+ return result;
+}
+
+CompositionText GetCompositionInfo(HIMC imm_context) {
+ // We only care about GCS_COMPATTR, GCS_COMPCLAUSE and GCS_CURSORPOS, and
+ // convert them into underlines and selection range respectively.
+
+ auto text = GetString(imm_context);
+
+ int length = static_cast<int>(text.length());
+ // Find out the range selected by the user.
+ int target_start = length;
+ int target_end = length;
+ GetCompositionTargetRange(imm_context, &target_start, &target_end);
+
+ auto clauses = GetCompositionClauses(imm_context, target_start, target_end);
+
+ int cursor = ::ImmGetCompositionString(imm_context, GCS_CURSORPOS, NULL, 0);
+
+ return CompositionText{std::move(text), std::move(clauses),
+ TextRange{cursor}};
+}
+
+} // namespace
+
+WinInputMethodContext::WinInputMethodContext(
+ gsl::not_null<WinNativeWindow*> window)
+ : native_window_(window) {
+ event_guard_ += window->NativeMessageEvent()->AddHandler(
+ std::bind(&WinInputMethodContext::OnWindowNativeMessage, this,
+ std::placeholders::_1));
+}
+
+WinInputMethodContext::~WinInputMethodContext() {}
+
+void WinInputMethodContext::EnableIME() {
+ const auto hwnd = native_window_->GetWindowHandle();
+ if (::ImmAssociateContextEx(hwnd, nullptr, IACE_DEFAULT) == FALSE) {
+ log::TagWarn(log_tag, u"Failed to enable ime.");
+ }
+}
+
+void WinInputMethodContext::DisableIME() {
+ const auto hwnd = native_window_->GetWindowHandle();
+ AutoHIMC himc{hwnd};
+
+ if (!::ImmNotifyIME(himc.Get(), NI_COMPOSITIONSTR, CPS_COMPLETE, 0)) {
+ log::TagWarn(log_tag,
+ u"Failed to complete composition before disable ime.");
+ }
+
+ if (::ImmAssociateContextEx(hwnd, nullptr, 0) == FALSE) {
+ log::TagWarn(log_tag, u"Failed to disable ime.");
+ }
+}
+
+void WinInputMethodContext::CompleteComposition() {
+ auto himc = GetHIMC();
+ if (!::ImmNotifyIME(himc.Get(), NI_COMPOSITIONSTR, CPS_COMPLETE, 0)) {
+ log::TagWarn(log_tag, u"Failed to complete composition.");
+ }
+}
+
+void WinInputMethodContext::CancelComposition() {
+ auto himc = GetHIMC();
+ if (!::ImmNotifyIME(himc.Get(), NI_COMPOSITIONSTR, CPS_CANCEL, 0)) {
+ log::TagWarn(log_tag, u"Failed to complete composition.");
+ }
+}
+
+CompositionText WinInputMethodContext::GetCompositionText() {
+ auto himc = GetHIMC();
+ return GetCompositionInfo(himc.Get());
+}
+
+void WinInputMethodContext::SetCandidateWindowPosition(const Point& point) {
+ auto himc = GetHIMC();
+
+ ::CANDIDATEFORM form;
+ form.dwIndex = 1;
+ form.dwStyle = CFS_CANDIDATEPOS;
+
+ form.ptCurrentPos = native_window_->DipToPixel(point);
+
+ if (!::ImmSetCandidateWindow(himc.Get(), &form))
+ log::TagDebug(log_tag,
+ u"Failed to set input method candidate window position.");
+}
+
+IEvent<std::nullptr_t>* WinInputMethodContext::CompositionStartEvent() {
+ return &composition_start_event_;
+}
+
+IEvent<std::nullptr_t>* WinInputMethodContext::CompositionEndEvent() {
+ return &composition_end_event_;
+};
+
+IEvent<std::nullptr_t>* WinInputMethodContext::CompositionEvent() {
+ return &composition_event_;
+}
+
+IEvent<std::u16string_view>* WinInputMethodContext::TextEvent() {
+ return &text_event_;
+}
+
+void WinInputMethodContext::OnWindowNativeMessage(
+ WindowNativeMessageEventArgs& args) {
+ const auto& message = args.GetWindowMessage();
+ switch (message.msg) {
+ case WM_CHAR: {
+ const auto c = static_cast<char16_t>(message.w_param);
+ if (IsUtf16SurrogatePairCodeUnit(c)) {
+ // I don't think this will happen because normal key strike without ime
+ // should only trigger ascci character. If it is a charater from
+ // supplementary planes, it should be handled with ime messages.
+ log::TagWarn(log_tag,
+ u"A WM_CHAR message for character from supplementary "
+ u"planes is ignored.");
+ } else {
+ char16_t s[1] = {c};
+ text_event_.Raise({s, 1});
+ }
+ args.HandleWithResult(0);
+ break;
+ }
+ case WM_IME_COMPOSITION: {
+ composition_event_.Raise(nullptr);
+ auto composition_text = GetCompositionText();
+ log::TagDebug(log_tag, u"WM_IME_COMPOSITION composition text:\n{}",
+ composition_text);
+ if (message.l_param & GCS_RESULTSTR) {
+ auto result_string = GetResultString();
+ text_event_.Raise(result_string);
+ }
+ break;
+ }
+ case WM_IME_STARTCOMPOSITION: {
+ composition_start_event_.Raise(nullptr);
+ break;
+ }
+ case WM_IME_ENDCOMPOSITION: {
+ composition_end_event_.Raise(nullptr);
+ break;
+ }
+ }
+}
+
+std::u16string WinInputMethodContext::GetResultString() {
+ auto himc = GetHIMC();
+ auto result = win::GetResultString(himc.Get());
+ return result;
+}
+
+AutoHIMC WinInputMethodContext::GetHIMC() {
+ const auto hwnd = native_window_->GetWindowHandle();
+ return AutoHIMC{hwnd};
+}
+} // namespace cru::platform::gui::win
diff --git a/src/win/gui/Keyboard.cpp b/src/win/gui/Keyboard.cpp
new file mode 100644
index 00000000..b706b240
--- /dev/null
+++ b/src/win/gui/Keyboard.cpp
@@ -0,0 +1,74 @@
+#include "cru/win/gui/Keyboard.hpp"
+
+namespace cru::platform::gui::win {
+KeyCode VirtualKeyToKeyCode(int virtual_key) {
+ if (virtual_key >= 0x30 && virtual_key <= 0x39) {
+ return KeyCode{static_cast<int>(KeyCode::N0) + (virtual_key - 0x30)};
+ } else if (virtual_key >= 0x41 && virtual_key <= 0x5a) {
+ return KeyCode{static_cast<int>(KeyCode::A) + (virtual_key - 0x41)};
+ } else if (virtual_key >= VK_NUMPAD0 && virtual_key <= VK_NUMPAD9) {
+ return KeyCode{static_cast<int>(KeyCode::NumPad0) +
+ (virtual_key - VK_NUMPAD0)};
+ } else if (virtual_key >= VK_F1 && virtual_key <= VK_F12) {
+ return KeyCode{static_cast<int>(KeyCode::F1) + (virtual_key - VK_F1)};
+ } else {
+ switch (virtual_key) {
+#define CRU_MAP_KEY(virtual_key, keycode) \
+ case virtual_key: \
+ return KeyCode::keycode;
+
+ CRU_MAP_KEY(VK_LBUTTON, LeftButton)
+ CRU_MAP_KEY(VK_MBUTTON, MiddleButton)
+ CRU_MAP_KEY(VK_RBUTTON, RightButton)
+ CRU_MAP_KEY(VK_ESCAPE, Escape)
+ CRU_MAP_KEY(VK_OEM_3, GraveAccent)
+ CRU_MAP_KEY(VK_TAB, Tab)
+ CRU_MAP_KEY(VK_CAPITAL, CapsLock)
+ CRU_MAP_KEY(VK_LSHIFT, LeftShift)
+ CRU_MAP_KEY(VK_LCONTROL, LeftCtrl)
+ CRU_MAP_KEY(VK_LWIN, LeftSuper)
+ CRU_MAP_KEY(VK_LMENU, LeftAlt)
+ CRU_MAP_KEY(VK_OEM_MINUS, Minus)
+ CRU_MAP_KEY(VK_OEM_PLUS, Equal)
+ CRU_MAP_KEY(VK_BACK, Backspace)
+ CRU_MAP_KEY(VK_OEM_4, LeftSquareBracket)
+ CRU_MAP_KEY(VK_OEM_6, RightSquareBracket)
+ CRU_MAP_KEY(VK_OEM_5, BackSlash)
+ CRU_MAP_KEY(VK_OEM_1, Semicolon)
+ CRU_MAP_KEY(VK_OEM_7, Quote)
+ CRU_MAP_KEY(VK_OEM_COMMA, Comma)
+ CRU_MAP_KEY(VK_OEM_PERIOD, Period)
+ CRU_MAP_KEY(VK_OEM_2, Slash)
+ CRU_MAP_KEY(VK_RSHIFT, RightShift)
+ CRU_MAP_KEY(VK_RCONTROL, RightCtrl)
+ CRU_MAP_KEY(VK_RWIN, RightSuper)
+ CRU_MAP_KEY(VK_RMENU, RightAlt)
+ CRU_MAP_KEY(VK_INSERT, Insert)
+ CRU_MAP_KEY(VK_DELETE, Delete)
+ CRU_MAP_KEY(VK_HOME, Home)
+ CRU_MAP_KEY(VK_END, End)
+ CRU_MAP_KEY(VK_PRIOR, PageUp)
+ CRU_MAP_KEY(VK_NEXT, PageDown)
+ CRU_MAP_KEY(VK_UP, Up)
+ CRU_MAP_KEY(VK_LEFT, Left)
+ CRU_MAP_KEY(VK_DOWN, Down)
+ CRU_MAP_KEY(VK_RIGHT, Right)
+ CRU_MAP_KEY(VK_SNAPSHOT, PrintScreen)
+ CRU_MAP_KEY(VK_PAUSE, Pause)
+
+#undef CRU_MAP_KEY
+
+ default:
+ return KeyCode::Unknown;
+ }
+ }
+}
+
+KeyModifier RetrieveKeyMofifier() {
+ KeyModifier result{0};
+ if (::GetKeyState(VK_SHIFT) < 0) result |= KeyModifiers::shift;
+ if (::GetKeyState(VK_CONTROL) < 0) result |= KeyModifiers::ctrl;
+ if (::GetKeyState(VK_MENU) < 0) result |= KeyModifiers::alt;
+ return result;
+}
+} // namespace cru::platform::gui::win
diff --git a/src/win/gui/TimerManager.cpp b/src/win/gui/TimerManager.cpp
new file mode 100644
index 00000000..fc26b6c4
--- /dev/null
+++ b/src/win/gui/TimerManager.cpp
@@ -0,0 +1,100 @@
+#include "TimerManager.hpp"
+
+#include "cru/win/gui/Base.hpp"
+#include "cru/win/gui/Exception.hpp"
+#include "gsl/gsl_util"
+
+#include <functional>
+#include <type_traits>
+
+namespace cru::platform::gui::win {
+constexpr int kSetImmediateWindowMessageId = WM_USER + 2000;
+
+TimerManager::TimerManager(GodWindow* god_window) {
+ god_window_ = god_window;
+ event_guard_ += god_window->MessageEvent()->AddHandler(std::bind(
+ &TimerManager::HandleGodWindowMessage, this, std::placeholders::_1));
+}
+
+long long TimerManager::SetTimer(TimerType type, int period,
+ std::function<void()> action) {
+ auto id = next_id_++;
+ TimerInfo timer_info{id, type, type == TimerType::Immediate ? 0 : period,
+ std::move(action)};
+ if (type == TimerType::Immediate) {
+ if (!::PostMessageW(god_window_->GetHandle(), kSetImmediateWindowMessageId,
+ gsl::narrow<UINT_PTR>(id), 0)) {
+ throw Win32Error(
+ ::GetLastError(),
+ "Failed to post window message to god window for set immediate.");
+ }
+ } else {
+ CreateNativeTimer(&timer_info);
+ }
+
+ info_map_.emplace(id, std::move(timer_info));
+ return id;
+}
+
+void TimerManager::CancelTimer(long long id) {
+ if (id <= 0) return;
+ auto find_result = this->info_map_.find(id);
+ if (find_result != info_map_.cend()) {
+ auto& info = find_result->second;
+ KillNativeTimer(&info);
+ this->info_map_.erase(find_result);
+ }
+}
+
+void TimerManager::CreateNativeTimer(TimerInfo* info) {
+ info->native_timer_id = gsl::narrow<UINT_PTR>(info->id);
+ ::SetTimer(god_window_->GetHandle(), info->native_timer_id, info->period,
+ nullptr);
+}
+
+void TimerManager::KillNativeTimer(TimerInfo* info) {
+ if (info->native_timer_id == 0) return;
+ ::KillTimer(god_window_->GetHandle(), info->native_timer_id);
+ info->native_timer_id = 0;
+}
+
+void TimerManager::HandleGodWindowMessage(WindowNativeMessageEventArgs& args) {
+ const auto& message = args.GetWindowMessage();
+
+ switch (message.msg) {
+ case kSetImmediateWindowMessageId: {
+ auto find_result =
+ this->info_map_.find(static_cast<long long>(message.w_param));
+ if (find_result != info_map_.cend()) {
+ auto& info = find_result->second;
+ info.action();
+ info_map_.erase(find_result);
+ }
+ args.SetResult(0);
+ args.SetHandled(true);
+ return;
+ }
+ case WM_TIMER: {
+ auto find_result =
+ this->info_map_.find(static_cast<long long>(message.w_param));
+ if (find_result != info_map_.cend()) {
+ auto& info = find_result->second;
+ if (info.type == TimerType::Interval) {
+ info.action();
+ args.SetResult(0);
+ args.SetHandled(true);
+ } else if (info.type == TimerType::Timeout) {
+ info.action();
+ KillNativeTimer(&info);
+ info_map_.erase(find_result);
+ args.SetResult(0);
+ args.SetHandled(true);
+ }
+ }
+ return;
+ }
+ default:
+ return;
+ }
+}
+} // namespace cru::platform::gui::win
diff --git a/src/win/gui/TimerManager.hpp b/src/win/gui/TimerManager.hpp
new file mode 100644
index 00000000..a8db1075
--- /dev/null
+++ b/src/win/gui/TimerManager.hpp
@@ -0,0 +1,61 @@
+#pragma once
+#include "cru/common/Event.hpp"
+#include "cru/win/WinPreConfig.hpp"
+
+#include "cru/common/Base.hpp"
+#include "cru/win/gui/GodWindow.hpp"
+#include "cru/win/gui/WindowNativeMessageEventArgs.hpp"
+
+#include <chrono>
+#include <functional>
+#include <optional>
+#include <unordered_map>
+
+namespace cru::platform::gui::win {
+enum class TimerType { Immediate, Timeout, Interval };
+
+struct TimerInfo {
+ TimerInfo(long long id, TimerType type, int period,
+ std::function<void()> action, UINT_PTR native_timer_id = 0)
+ : id(id),
+ type(type),
+ period(period),
+ action(std::move(action)),
+ native_timer_id(native_timer_id) {}
+
+ long long id;
+ TimerType type;
+ int period; // in milliseconds
+ std::function<void()> action;
+ UINT_PTR native_timer_id;
+};
+
+class TimerManager : public Object {
+ public:
+ TimerManager(GodWindow* god_window);
+
+ CRU_DELETE_COPY(TimerManager)
+ CRU_DELETE_MOVE(TimerManager)
+
+ ~TimerManager() override = default;
+
+ // Period is in milliseconds. When type is immediate, it is not checked and
+ // used.
+ long long SetTimer(TimerType type, int period, std::function<void()> action);
+ void CancelTimer(long long id);
+
+ private:
+ void HandleGodWindowMessage(WindowNativeMessageEventArgs& args);
+
+ void CreateNativeTimer(TimerInfo* info);
+ void KillNativeTimer(TimerInfo* info);
+
+ private:
+ GodWindow* god_window_;
+
+ EventRevokerListGuard event_guard_;
+
+ long long next_id_ = 1;
+ std::unordered_map<long long, TimerInfo> info_map_;
+};
+} // namespace cru::platform::gui::win
diff --git a/src/win/gui/UiApplication.cpp b/src/win/gui/UiApplication.cpp
new file mode 100644
index 00000000..5041a6c0
--- /dev/null
+++ b/src/win/gui/UiApplication.cpp
@@ -0,0 +1,118 @@
+#include "cru/win/gui/UiApplication.hpp"
+
+#include "../DebugLogger.hpp"
+#include "../StdOutLogger.hpp"
+#include "TimerManager.hpp"
+#include "WindowManager.hpp"
+#include "cru/common/Logger.hpp"
+#include "cru/platform/Check.hpp"
+#include "cru/win/graphics/direct/Factory.hpp"
+#include "cru/win/gui/Cursor.hpp"
+#include "cru/win/gui/Exception.hpp"
+#include "cru/win/gui/GodWindow.hpp"
+#include "cru/win/gui/InputMethod.hpp"
+#include "cru/win/gui/Window.hpp"
+
+namespace cru::platform::gui {
+std::unique_ptr<IUiApplication> CreateUiApplication() {
+ return std::make_unique<win::WinUiApplication>();
+}
+} // namespace cru::platform::gui
+
+namespace cru::platform::gui::win {
+WinUiApplication* WinUiApplication::instance = nullptr;
+
+WinUiApplication::WinUiApplication() {
+ instance = this;
+
+ instance_handle_ = ::GetModuleHandleW(nullptr);
+ if (!instance_handle_)
+ throw Win32Error("Failed to get module(instance) handle.");
+
+ ::SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE);
+
+ log::Logger::GetInstance()->AddSource(
+ std::make_unique<::cru::platform::win::WinDebugLoggerSource>());
+ log::Logger::GetInstance()->AddSource(
+ std::make_unique<::cru::platform::win::WinStdOutLoggerSource>());
+
+ graph_factory_ =
+ std::make_unique<cru::platform::graphics::win::direct::DirectGraphFactory>();
+
+ god_window_ = std::make_unique<GodWindow>(this);
+ timer_manager_ = std::make_unique<TimerManager>(god_window_.get());
+ window_manager_ = std::make_unique<WindowManager>(this);
+ cursor_manager_ = std::make_unique<WinCursorManager>();
+}
+
+WinUiApplication::~WinUiApplication() { instance = nullptr; }
+
+int WinUiApplication::Run() {
+ MSG msg;
+ while (GetMessageW(&msg, nullptr, 0, 0)) {
+ TranslateMessage(&msg);
+ DispatchMessageW(&msg);
+ }
+
+ for (const auto& handler : quit_handlers_) handler();
+
+ return static_cast<int>(msg.wParam);
+}
+
+void WinUiApplication::RequestQuit(const int quit_code) {
+ ::PostQuitMessage(quit_code);
+}
+
+void WinUiApplication::AddOnQuitHandler(std::function<void()> handler) {
+ quit_handlers_.push_back(std::move(handler));
+}
+
+long long WinUiApplication::SetImmediate(std::function<void()> action) {
+ return this->timer_manager_->SetTimer(TimerType::Immediate, 0,
+ std::move(action));
+}
+
+long long WinUiApplication::SetTimeout(std::chrono::milliseconds milliseconds,
+ std::function<void()> action) {
+ return this->timer_manager_->SetTimer(TimerType::Timeout,
+ gsl::narrow<int>(milliseconds.count()),
+ std::move(action));
+}
+
+long long WinUiApplication::SetInterval(std::chrono::milliseconds milliseconds,
+ std::function<void()> action) {
+ return this->timer_manager_->SetTimer(TimerType::Interval,
+ gsl::narrow<int>(milliseconds.count()),
+ std::move(action));
+}
+
+void WinUiApplication::CancelTimer(long long id) {
+ timer_manager_->CancelTimer(id);
+}
+
+std::vector<INativeWindow*> WinUiApplication::GetAllWindow() {
+ const auto&& windows = window_manager_->GetAllWindows();
+ std::vector<INativeWindow*> result;
+ for (const auto w : windows) {
+ result.push_back(static_cast<INativeWindow*>(w));
+ }
+ return result;
+}
+
+INativeWindow* WinUiApplication::CreateWindow(INativeWindow* parent) {
+ WinNativeWindow* p = nullptr;
+ if (parent != nullptr) {
+ p = CheckPlatform<WinNativeWindow>(parent, GetPlatformId());
+ }
+ return new WinNativeWindow(this, window_manager_->GetGeneralWindowClass(),
+ WS_OVERLAPPEDWINDOW, p);
+}
+
+cru::platform::graphics::IGraphFactory* WinUiApplication::GetGraphFactory() {
+ return graph_factory_.get();
+}
+
+ICursorManager* WinUiApplication::GetCursorManager() {
+ return cursor_manager_.get();
+}
+} // namespace cru::platform::gui::win
diff --git a/src/win/gui/Window.cpp b/src/win/gui/Window.cpp
new file mode 100644
index 00000000..dda8a36a
--- /dev/null
+++ b/src/win/gui/Window.cpp
@@ -0,0 +1,453 @@
+#include "cru/win/gui/Window.hpp"
+
+#include "WindowManager.hpp"
+#include "cru/common/Logger.hpp"
+#include "cru/platform/Check.hpp"
+#include "cru/platform/gui/Base.hpp"
+#include "cru/win/graphics/direct/WindowPainter.hpp"
+#include "cru/win/gui/Cursor.hpp"
+#include "cru/win/gui/Exception.hpp"
+#include "cru/win/gui/InputMethod.hpp"
+#include "cru/win/gui/Keyboard.hpp"
+#include "cru/win/gui/UiApplication.hpp"
+#include "cru/win/gui/WindowClass.hpp"
+
+#include <imm.h>
+#include <windowsx.h>
+#include <memory>
+
+namespace cru::platform::gui::win {
+WinNativeWindow::WinNativeWindow(WinUiApplication* application,
+ WindowClass* window_class, DWORD window_style,
+ WinNativeWindow* parent)
+ : application_(application), parent_window_(parent) {
+ Expects(application); // application can't be null.
+
+ if (parent != nullptr) {
+ throw new std::runtime_error("Can't use a invalid window as parent.");
+ }
+
+ const auto window_manager = application->GetWindowManager();
+
+ hwnd_ = CreateWindowExW(
+ 0, window_class->GetName(), L"", window_style, CW_USEDEFAULT,
+ CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
+ parent == nullptr ? nullptr : parent->GetWindowHandle(), nullptr,
+ application->GetInstanceHandle(), nullptr);
+
+ if (hwnd_ == nullptr)
+ throw Win32Error(::GetLastError(), "Failed to create window.");
+
+ auto dpi = ::GetDpiForWindow(hwnd_);
+ if (dpi == 0)
+ throw Win32Error(::GetLastError(), "Failed to get dpi of window.");
+ dpi_ = static_cast<float>(dpi);
+ log::Debug(u"Dpi of window is {}.", dpi_);
+
+ window_manager->RegisterWindow(hwnd_, this);
+
+ SetCursor(application->GetCursorManager()->GetSystemCursor(
+ cru::platform::gui::SystemCursorType::Arrow));
+
+ window_render_target_ =
+ std::make_unique<graphics::win::direct::D2DWindowRenderTarget>(
+ application->GetDirectFactory(), hwnd_);
+ window_render_target_->SetDpi(dpi_, dpi_);
+
+ input_method_context_ = std::make_unique<WinInputMethodContext>(this);
+ input_method_context_->DisableIME();
+}
+
+WinNativeWindow::~WinNativeWindow() {
+ if (!sync_flag_) {
+ sync_flag_ = true;
+ Close();
+ }
+}
+
+void WinNativeWindow::Close() { ::DestroyWindow(hwnd_); }
+
+bool WinNativeWindow::IsVisible() { return ::IsWindowVisible(hwnd_); }
+
+void WinNativeWindow::SetVisible(bool is_visible) {
+ is_visible ? ShowWindow(hwnd_, SW_SHOWNORMAL) : ShowWindow(hwnd_, SW_HIDE);
+}
+Size WinNativeWindow::GetClientSize() {
+ const auto pixel_rect = GetClientRectPixel();
+ return Size(PixelToDip(pixel_rect.right), PixelToDip(pixel_rect.bottom));
+}
+
+void WinNativeWindow::SetClientSize(const Size& size) {
+ const auto window_style =
+ static_cast<DWORD>(GetWindowLongPtr(hwnd_, GWL_STYLE));
+ const auto window_ex_style =
+ static_cast<DWORD>(GetWindowLongPtr(hwnd_, GWL_EXSTYLE));
+
+ RECT rect;
+ rect.left = 0;
+ rect.top = 0;
+ rect.right = DipToPixel(size.width);
+ rect.bottom = DipToPixel(size.height);
+ if (!AdjustWindowRectEx(&rect, window_style, FALSE, window_ex_style))
+ throw Win32Error(::GetLastError(), "Failed to invoke AdjustWindowRectEx.");
+
+ if (!SetWindowPos(hwnd_, nullptr, 0, 0, rect.right - rect.left,
+ rect.bottom - rect.top, SWP_NOZORDER | SWP_NOMOVE))
+ throw Win32Error(::GetLastError(), "Failed to invoke SetWindowPos.");
+}
+
+Rect WinNativeWindow::GetWindowRect() {
+ RECT rect;
+ if (!::GetWindowRect(hwnd_, &rect))
+ throw Win32Error(::GetLastError(), "Failed to invoke GetWindowRect.");
+
+ return Rect::FromVertices(PixelToDip(rect.left), PixelToDip(rect.top),
+ PixelToDip(rect.right), PixelToDip(rect.bottom));
+}
+
+void WinNativeWindow::SetWindowRect(const Rect& rect) {
+ if (!SetWindowPos(hwnd_, nullptr, DipToPixel(rect.left), DipToPixel(rect.top),
+ DipToPixel(rect.GetRight()), DipToPixel(rect.GetBottom()),
+ SWP_NOZORDER))
+ throw Win32Error(::GetLastError(), "Failed to invoke SetWindowPos.");
+}
+
+Point WinNativeWindow::GetMousePosition() {
+ POINT p;
+ if (!::GetCursorPos(&p))
+ throw Win32Error(::GetLastError(), "Failed to get cursor position.");
+ if (!::ScreenToClient(hwnd_, &p))
+ throw Win32Error(::GetLastError(), "Failed to call ScreenToClient.");
+ return PixelToDip(p);
+}
+
+bool WinNativeWindow::CaptureMouse() {
+ ::SetCapture(hwnd_);
+ return true;
+}
+
+bool WinNativeWindow::ReleaseMouse() {
+ const auto result = ::ReleaseCapture();
+ return result != 0;
+}
+
+void WinNativeWindow::RequestRepaint() {
+ log::TagDebug(log_tag, u"A repaint is requested.");
+ if (!::InvalidateRect(hwnd_, nullptr, FALSE))
+ throw Win32Error(::GetLastError(), "Failed to invalidate window.");
+ if (!::UpdateWindow(hwnd_))
+ throw Win32Error(::GetLastError(), "Failed to update window.");
+}
+
+std::unique_ptr<graphics::IPainter> WinNativeWindow::BeginPaint() {
+ return std::make_unique<graphics::win::direct::D2DWindowPainter>(
+ window_render_target_.get());
+}
+
+void WinNativeWindow::SetCursor(std::shared_ptr<ICursor> cursor) {
+ if (cursor == nullptr) {
+ throw std::runtime_error("Can't use a nullptr as cursor.");
+ }
+
+ cursor_ = CheckPlatform<WinCursor>(cursor, GetPlatformId());
+
+ if (!::SetClassLongPtrW(hwnd_, GCLP_HCURSOR,
+ reinterpret_cast<LONG_PTR>(cursor_->GetHandle()))) {
+ log::TagWarn(log_tag,
+ u"Failed to set cursor because failed to set class long. Last "
+ u"error code: {}.",
+ ::GetLastError());
+ return;
+ }
+
+ if (!IsVisible()) return;
+
+ auto lg = [](const std::u16string_view& reason) {
+ log::TagWarn(
+ log_tag,
+ u"Failed to set cursor because {} when window is visible. (We need to "
+ u"update cursor if it is inside the window.) Last error code: {}.",
+ reason, ::GetLastError());
+ };
+
+ ::POINT point;
+ if (!::GetCursorPos(&point)) {
+ lg(u"failed to get cursor pos");
+ return;
+ }
+
+ ::RECT rect;
+ if (!::GetClientRect(hwnd_, &rect)) {
+ lg(u"failed to get window's client rect");
+ return;
+ }
+
+ ::POINT lefttop{rect.left, rect.top};
+ ::POINT rightbottom{rect.right, rect.bottom};
+ if (!::ClientToScreen(hwnd_, &lefttop)) {
+ lg(u"failed to call ClientToScreen on lefttop");
+ return;
+ }
+
+ if (!::ClientToScreen(hwnd_, &rightbottom)) {
+ lg(u"failed to call ClientToScreen on rightbottom");
+ return;
+ }
+
+ if (point.x >= lefttop.x && point.y >= lefttop.y &&
+ point.x <= rightbottom.x && point.y <= rightbottom.y) {
+ ::SetCursor(cursor_->GetHandle());
+ }
+}
+
+IInputMethodContext* WinNativeWindow::GetInputMethodContext() {
+ return static_cast<IInputMethodContext*>(input_method_context_.get());
+}
+
+bool WinNativeWindow::HandleNativeWindowMessage(HWND hwnd, UINT msg,
+ WPARAM w_param, LPARAM l_param,
+ LRESULT* result) {
+ WindowNativeMessageEventArgs args{
+ WindowNativeMessage{hwnd, msg, w_param, l_param}};
+ native_message_event_.Raise(args);
+ if (args.IsHandled()) {
+ *result = args.GetResult();
+ return true;
+ }
+
+ switch (msg) {
+ case WM_PAINT:
+ OnPaintInternal();
+ *result = 0;
+ return true;
+ case WM_ERASEBKGND:
+ *result = 1;
+ return true;
+ case WM_SETFOCUS:
+ OnSetFocusInternal();
+ *result = 0;
+ return true;
+ case WM_KILLFOCUS:
+ OnKillFocusInternal();
+ *result = 0;
+ return true;
+ case WM_MOUSEMOVE: {
+ POINT point;
+ point.x = GET_X_LPARAM(l_param);
+ point.y = GET_Y_LPARAM(l_param);
+ OnMouseMoveInternal(point);
+ *result = 0;
+ return true;
+ }
+ case WM_LBUTTONDOWN: {
+ POINT point;
+ point.x = GET_X_LPARAM(l_param);
+ point.y = GET_Y_LPARAM(l_param);
+ OnMouseDownInternal(platform::gui::mouse_buttons::left, point);
+ *result = 0;
+ return true;
+ }
+ case WM_LBUTTONUP: {
+ POINT point;
+ point.x = GET_X_LPARAM(l_param);
+ point.y = GET_Y_LPARAM(l_param);
+ OnMouseUpInternal(platform::gui::mouse_buttons::left, point);
+ *result = 0;
+ return true;
+ }
+ case WM_RBUTTONDOWN: {
+ POINT point;
+ point.x = GET_X_LPARAM(l_param);
+ point.y = GET_Y_LPARAM(l_param);
+ OnMouseDownInternal(platform::gui::mouse_buttons::right, point);
+ *result = 0;
+ return true;
+ }
+ case WM_RBUTTONUP: {
+ POINT point;
+ point.x = GET_X_LPARAM(l_param);
+ point.y = GET_Y_LPARAM(l_param);
+ OnMouseUpInternal(platform::gui::mouse_buttons::right, point);
+ *result = 0;
+ return true;
+ }
+ case WM_MBUTTONDOWN: {
+ POINT point;
+ point.x = GET_X_LPARAM(l_param);
+ point.y = GET_Y_LPARAM(l_param);
+ OnMouseDownInternal(platform::gui::mouse_buttons::middle, point);
+ *result = 0;
+ return true;
+ }
+ case WM_MBUTTONUP: {
+ POINT point;
+ point.x = GET_X_LPARAM(l_param);
+ point.y = GET_Y_LPARAM(l_param);
+ OnMouseUpInternal(platform::gui::mouse_buttons::middle, point);
+ *result = 0;
+ return true;
+ }
+ case WM_MOUSEWHEEL:
+ POINT point;
+ point.x = GET_X_LPARAM(l_param);
+ point.y = GET_Y_LPARAM(l_param);
+ ScreenToClient(hwnd, &point);
+ OnMouseWheelInternal(GET_WHEEL_DELTA_WPARAM(w_param), point);
+ *result = 0;
+ return true;
+ case WM_KEYDOWN:
+ OnKeyDownInternal(static_cast<int>(w_param));
+ *result = 0;
+ return true;
+ case WM_KEYUP:
+ OnKeyUpInternal(static_cast<int>(w_param));
+ *result = 0;
+ return true;
+ case WM_SYSKEYDOWN:
+ if (l_param & (1 << 29)) {
+ OnKeyDownInternal(static_cast<int>(w_param));
+ *result = 0;
+ return true;
+ }
+ return false;
+ case WM_SYSKEYUP:
+ if (l_param & (1 << 29)) {
+ OnKeyUpInternal(static_cast<int>(w_param));
+ *result = 0;
+ return true;
+ }
+ return false;
+ case WM_SIZE:
+ OnResizeInternal(LOWORD(l_param), HIWORD(l_param));
+ *result = 0;
+ return true;
+ case WM_ACTIVATE:
+ if (w_param == WA_ACTIVE || w_param == WA_CLICKACTIVE)
+ OnActivatedInternal();
+ else if (w_param == WA_INACTIVE)
+ OnDeactivatedInternal();
+ *result = 0;
+ return true;
+ case WM_DESTROY:
+ OnDestroyInternal();
+ *result = 0;
+ return true;
+ case WM_IME_SETCONTEXT:
+ l_param &= ~ISC_SHOWUICOMPOSITIONWINDOW;
+ *result = ::DefWindowProcW(hwnd, msg, w_param, l_param);
+ return true;
+ // We must block these message from DefWindowProc or it will create
+ // an ugly composition window.
+ case WM_IME_STARTCOMPOSITION:
+ case WM_IME_COMPOSITION:
+ *result = 0;
+ return true;
+ case WM_DPICHANGED: {
+ dpi_ = static_cast<float>(LOWORD(w_param));
+ const RECT* suggest_rect = reinterpret_cast<const RECT*>(l_param);
+ window_render_target_->SetDpi(dpi_, dpi_);
+ SetWindowPos(hwnd_, NULL, suggest_rect->left, suggest_rect->top,
+ suggest_rect->right - suggest_rect->left,
+ suggest_rect->bottom - suggest_rect->top,
+ SWP_NOZORDER | SWP_NOACTIVATE);
+ }
+ default:
+ return false;
+ }
+}
+
+RECT WinNativeWindow::GetClientRectPixel() {
+ RECT rect;
+ if (!GetClientRect(hwnd_, &rect))
+ throw Win32Error(::GetLastError(), "Failed to invoke GetClientRect.");
+ return rect;
+}
+
+void WinNativeWindow::OnDestroyInternal() {
+ application_->GetWindowManager()->UnregisterWindow(hwnd_);
+ hwnd_ = nullptr;
+ destroy_event_.Raise(nullptr);
+ if (!sync_flag_) {
+ sync_flag_ = true;
+ delete this;
+ }
+}
+
+void WinNativeWindow::OnPaintInternal() {
+ paint_event_.Raise(nullptr);
+ ValidateRect(hwnd_, nullptr);
+ log::TagDebug(log_tag, u"A repaint is finished.");
+}
+
+void WinNativeWindow::OnResizeInternal(const int new_width,
+ const int new_height) {
+ if (!(new_width == 0 && new_height == 0)) {
+ window_render_target_->ResizeBuffer(new_width, new_height);
+ resize_event_.Raise(Size{PixelToDip(new_width), PixelToDip(new_height)});
+ }
+}
+
+void WinNativeWindow::OnSetFocusInternal() {
+ has_focus_ = true;
+ focus_event_.Raise(FocusChangeType::Gain);
+}
+
+void WinNativeWindow::OnKillFocusInternal() {
+ has_focus_ = false;
+ focus_event_.Raise(FocusChangeType::Lost);
+}
+
+void WinNativeWindow::OnMouseMoveInternal(const POINT point) {
+ // when mouse was previous outside the window
+ if (!is_mouse_in_) {
+ // invoke TrackMouseEvent to have WM_MOUSELEAVE sent.
+ TRACKMOUSEEVENT tme;
+ tme.cbSize = sizeof tme;
+ tme.dwFlags = TME_LEAVE;
+ tme.hwndTrack = hwnd_;
+
+ TrackMouseEvent(&tme);
+
+ is_mouse_in_ = true;
+ mouse_enter_leave_event_.Raise(MouseEnterLeaveType::Enter);
+ }
+
+ mouse_move_event_.Raise(PixelToDip(point));
+}
+
+void WinNativeWindow::OnMouseLeaveInternal() {
+ is_mouse_in_ = false;
+ mouse_enter_leave_event_.Raise(MouseEnterLeaveType::Leave);
+}
+
+void WinNativeWindow::OnMouseDownInternal(platform::gui::MouseButton button,
+ POINT point) {
+ const auto dip_point = PixelToDip(point);
+ mouse_down_event_.Raise({button, dip_point, RetrieveKeyMofifier()});
+}
+
+void WinNativeWindow::OnMouseUpInternal(platform::gui::MouseButton button,
+ POINT point) {
+ const auto dip_point = PixelToDip(point);
+ mouse_up_event_.Raise({button, dip_point, RetrieveKeyMofifier()});
+}
+
+void WinNativeWindow::OnMouseWheelInternal(short delta, POINT point) {
+ CRU_UNUSED(delta)
+ CRU_UNUSED(point)
+}
+
+void WinNativeWindow::OnKeyDownInternal(int virtual_code) {
+ key_down_event_.Raise(
+ {VirtualKeyToKeyCode(virtual_code), RetrieveKeyMofifier()});
+}
+
+void WinNativeWindow::OnKeyUpInternal(int virtual_code) {
+ key_up_event_.Raise(
+ {VirtualKeyToKeyCode(virtual_code), RetrieveKeyMofifier()});
+}
+
+void WinNativeWindow::OnActivatedInternal() {}
+
+void WinNativeWindow::OnDeactivatedInternal() {}
+} // namespace cru::platform::gui::win
diff --git a/src/win/gui/WindowClass.cpp b/src/win/gui/WindowClass.cpp
new file mode 100644
index 00000000..a033d091
--- /dev/null
+++ b/src/win/gui/WindowClass.cpp
@@ -0,0 +1,28 @@
+#include "cru/win/gui/WindowClass.hpp"
+
+#include "cru/win/gui/Exception.hpp"
+
+namespace cru::platform::gui::win {
+WindowClass::WindowClass(std::wstring name, WNDPROC window_proc,
+ HINSTANCE h_instance)
+ : name_(std::move(name)) {
+ WNDCLASSEXW window_class;
+ window_class.cbSize = sizeof(WNDCLASSEXW);
+
+ window_class.style = CS_HREDRAW | CS_VREDRAW;
+ window_class.lpfnWndProc = window_proc;
+ window_class.cbClsExtra = 0;
+ window_class.cbWndExtra = 0;
+ window_class.hInstance = h_instance;
+ window_class.hIcon = LoadIcon(NULL, IDI_APPLICATION);
+ window_class.hCursor = LoadCursor(NULL, IDC_ARROW);
+ window_class.hbrBackground = GetSysColorBrush(COLOR_BTNFACE);
+ window_class.lpszMenuName = NULL;
+ window_class.lpszClassName = name_.c_str();
+ window_class.hIconSm = NULL;
+
+ atom_ = ::RegisterClassExW(&window_class);
+ if (atom_ == 0)
+ throw Win32Error(::GetLastError(), "Failed to create window class.");
+}
+} // namespace cru::platform::gui::win
diff --git a/src/win/gui/WindowManager.cpp b/src/win/gui/WindowManager.cpp
new file mode 100644
index 00000000..4e84e967
--- /dev/null
+++ b/src/win/gui/WindowManager.cpp
@@ -0,0 +1,56 @@
+#include "WindowManager.hpp"
+
+#include "cru/win/gui/UiApplication.hpp"
+#include "cru/win/gui/Window.hpp"
+#include "cru/win/gui/WindowClass.hpp"
+
+namespace cru::platform::gui::win {
+LRESULT __stdcall GeneralWndProc(HWND hWnd, UINT Msg, WPARAM wParam,
+ LPARAM lParam) {
+ auto window =
+ WinUiApplication::GetInstance()->GetWindowManager()->FromHandle(hWnd);
+
+ LRESULT result;
+ if (window != nullptr &&
+ window->HandleNativeWindowMessage(hWnd, Msg, wParam, lParam, &result))
+ return result;
+
+ return DefWindowProc(hWnd, Msg, wParam, lParam);
+}
+
+WindowManager::WindowManager(WinUiApplication* application) {
+ application_ = application;
+ general_window_class_ = std::make_unique<WindowClass>(
+ L"CruUIWindowClass", GeneralWndProc, application->GetInstanceHandle());
+}
+
+WindowManager::~WindowManager() {
+ for (const auto& [key, window] : window_map_) delete window;
+}
+
+void WindowManager::RegisterWindow(HWND hwnd, WinNativeWindow* window) {
+ Expects(window_map_.count(hwnd) == 0); // The hwnd is already in the map.
+ window_map_.emplace(hwnd, window);
+}
+
+void WindowManager::UnregisterWindow(HWND hwnd) {
+ const auto find_result = window_map_.find(hwnd);
+ Expects(find_result != window_map_.end()); // The hwnd is not in the map.
+ window_map_.erase(find_result);
+ if (window_map_.empty()) application_->RequestQuit(0);
+}
+
+WinNativeWindow* WindowManager::FromHandle(HWND hwnd) {
+ const auto find_result = window_map_.find(hwnd);
+ if (find_result == window_map_.end())
+ return nullptr;
+ else
+ return find_result->second;
+}
+
+std::vector<WinNativeWindow*> WindowManager::GetAllWindows() const {
+ std::vector<WinNativeWindow*> windows;
+ for (const auto& [key, value] : window_map_) windows.push_back(value);
+ return windows;
+}
+} // namespace cru::platform::gui::win
diff --git a/src/win/gui/WindowManager.hpp b/src/win/gui/WindowManager.hpp
new file mode 100644
index 00000000..3b037f89
--- /dev/null
+++ b/src/win/gui/WindowManager.hpp
@@ -0,0 +1,51 @@
+#pragma once
+#include "cru/win/WinPreConfig.hpp"
+
+#include "cru/common/Base.hpp"
+
+#include <map>
+#include <memory>
+#include <vector>
+
+namespace cru::platform::gui::win {
+class WinUiApplication;
+class WinNativeWindow;
+class WindowClass;
+
+class WindowManager : public Object {
+ public:
+ WindowManager(WinUiApplication* application);
+
+ CRU_DELETE_COPY(WindowManager)
+ CRU_DELETE_MOVE(WindowManager)
+
+ ~WindowManager() override;
+
+ // Get the general window class for creating ordinary window.
+ WindowClass* GetGeneralWindowClass() const {
+ return general_window_class_.get();
+ }
+
+ // Register a window newly created.
+ // This function adds the hwnd to hwnd-window map.
+ // It should be called immediately after a window was created.
+ void RegisterWindow(HWND hwnd, WinNativeWindow* window);
+
+ // Unregister a window that is going to be destroyed.
+ // This function removes the hwnd from the hwnd-window map.
+ // It should be called immediately before a window is going to be destroyed,
+ void UnregisterWindow(HWND hwnd);
+
+ // Return a pointer to the Window object related to the HWND or nullptr if the
+ // hwnd is not in the map.
+ WinNativeWindow* FromHandle(HWND hwnd);
+
+ std::vector<WinNativeWindow*> GetAllWindows() const;
+
+ private:
+ WinUiApplication* application_;
+
+ std::unique_ptr<WindowClass> general_window_class_;
+ std::map<HWND, WinNativeWindow*> window_map_;
+};
+} // namespace cru::platform::gui::win