From 548330b8cf62005ded1b591b68c72953fc66cf28 Mon Sep 17 00:00:00 2001 From: fanlusky <1730976608@qq.com> Date: Wed, 15 Jul 2026 18:22:50 +0800 Subject: [PATCH] Make a demo of an on-screen keyboard. --- ui/CMakeLists.txt | 11 + ui/README.md | 16 + .../msimeui-keyboard-demo/KeyboardPanel.cpp | 275 ++++++++++++++++++ .../msimeui-keyboard-demo/KeyboardPanel.h | 55 ++++ ui/demos/msimeui-keyboard-demo/main.cpp | 37 +++ .../msimeui-keyboard-demo.rc | 3 + ui/demos/msimeui-keyboard-demo/resource.h | 3 + ui/include/msimeui/Window.h | 1 + ui/scripts/build-keyboard-demo.ps1 | 26 ++ ui/scripts/launch-keyboard-demo.ps1 | 21 ++ ui/src/Window.cpp | 22 ++ 11 files changed, 470 insertions(+) create mode 100644 ui/demos/msimeui-keyboard-demo/KeyboardPanel.cpp create mode 100644 ui/demos/msimeui-keyboard-demo/KeyboardPanel.h create mode 100644 ui/demos/msimeui-keyboard-demo/main.cpp create mode 100644 ui/demos/msimeui-keyboard-demo/msimeui-keyboard-demo.rc create mode 100644 ui/demos/msimeui-keyboard-demo/resource.h create mode 100644 ui/scripts/build-keyboard-demo.ps1 create mode 100644 ui/scripts/launch-keyboard-demo.ps1 diff --git a/ui/CMakeLists.txt b/ui/CMakeLists.txt index 2a3141c0..b3a048c6 100644 --- a/ui/CMakeLists.txt +++ b/ui/CMakeLists.txt @@ -95,8 +95,19 @@ add_executable(msimeui-emoji-panel WIN32 target_link_libraries(msimeui-emoji-panel PRIVATE msimeui) +add_executable(msimeui-keyboard-demo WIN32 + demos/msimeui-keyboard-demo/KeyboardPanel.cpp + demos/msimeui-keyboard-demo/KeyboardPanel.h + demos/msimeui-keyboard-demo/main.cpp + demos/msimeui-keyboard-demo/msimeui-keyboard-demo.rc + demos/msimeui-keyboard-demo/resource.h +) + +target_link_libraries(msimeui-keyboard-demo PRIVATE msimeui) + if(MSVC) target_compile_options(msimeui PRIVATE /W4 /permissive-) target_compile_options(msimeui-demo PRIVATE /W4 /permissive-) target_compile_options(msimeui-emoji-panel PRIVATE /W4 /permissive-) + target_compile_options(msimeui-keyboard-demo PRIVATE /W4 /permissive-) endif() diff --git a/ui/README.md b/ui/README.md index 4a456f87..9f823d3f 100644 --- a/ui/README.md +++ b/ui/README.md @@ -208,6 +208,22 @@ cmake --build build --config Debug ./scripts/launch-emoji-panel.ps1 -Build ``` +### 暗色屏幕键盘 Demo + +独立的 `msimeui-keyboard-demo.exe` 提供五行暗色 PC 键盘布局、按键悬浮/按压/修饰键状态, +并使用 `SendInput` 将字符发送到当前前台应用: + +```powershell +./scripts/build-keyboard-demo.ps1 +./scripts/launch-keyboard-demo.ps1 +``` + +或者一次构建并启动: + +```powershell +./scripts/launch-keyboard-demo.ps1 -Build +``` + 如果切换过 CMake generator、Visual Studio 版本或者 preset,可以加 `-Fresh` 重新生成构建目录: ```powershell diff --git a/ui/demos/msimeui-keyboard-demo/KeyboardPanel.cpp b/ui/demos/msimeui-keyboard-demo/KeyboardPanel.cpp new file mode 100644 index 00000000..c6a261c6 --- /dev/null +++ b/ui/demos/msimeui-keyboard-demo/KeyboardPanel.cpp @@ -0,0 +1,275 @@ +#include "KeyboardPanel.h" + +#include "msimeui/DeviceResources.h" +#include "msimeui/Window.h" + +#include +#include + +namespace msimeui +{ +namespace +{ +constexpr float kHeaderHeight = 28.0f; +constexpr float kOuterPadding = 7.0f; +constexpr float kGap = 4.0f; +constexpr size_t kInvalidIndex = static_cast(-1); + +bool Contains(const RectF &rect, const PointF &point) +{ + return point.x >= rect.x && point.x <= rect.x + rect.width && point.y >= rect.y && point.y <= rect.y + rect.height; +} + +void FillRounded(DeviceResources &resources, const RectF &rect, const D2D1_COLOR_F &color, float radius) +{ + auto *target = resources.GetRenderTarget(); + auto *brush = resources.GetSolidColorBrush(color); + if (target && brush) + { + target->FillRoundedRectangle( + D2D1::RoundedRect(D2D1::RectF(rect.x, rect.y, rect.x + rect.width, rect.y + rect.height), radius, radius), brush); + } +} + +void DrawLabel(DeviceResources &resources, const std::wstring &text, const RectF &rect, float size, + const D2D1_COLOR_F &color, DWRITE_TEXT_ALIGNMENT alignment = DWRITE_TEXT_ALIGNMENT_CENTER) +{ + auto *target = resources.GetRenderTarget(); + auto *format = resources.GetTextFormat(L"Segoe UI", size, DWRITE_FONT_WEIGHT_NORMAL, alignment, + DWRITE_PARAGRAPH_ALIGNMENT_CENTER, DWRITE_WORD_WRAPPING_NO_WRAP); + auto *brush = resources.GetSolidColorBrush(color); + if (target && format && brush) + { + target->DrawTextW(text.c_str(), static_cast(text.size()), format, + D2D1::RectF(rect.x, rect.y, rect.x + rect.width, rect.y + rect.height), brush, + D2D1_DRAW_TEXT_OPTIONS_CLIP); + } +} + +void DrawCloseIcon(DeviceResources &resources, const RectF &rect, const D2D1_COLOR_F &color) +{ + auto *target = resources.GetRenderTarget(); + auto *brush = resources.GetSolidColorBrush(color); + if (!target || !brush) + { + return; + } + + const float centerX = rect.x + rect.width * 0.5f; + const float centerY = rect.y + rect.height * 0.5f; + const float shortSide = std::min(rect.width, rect.height); + const float halfLength = shortSide * 0.22f; + const float strokeWidth = std::max(shortSide * 0.075f, 1.0f); + target->DrawLine(D2D1::Point2F(centerX - halfLength, centerY - halfLength), + D2D1::Point2F(centerX + halfLength, centerY + halfLength), brush, strokeWidth); + target->DrawLine(D2D1::Point2F(centerX + halfLength, centerY - halfLength), + D2D1::Point2F(centerX - halfLength, centerY + halfLength), brush, strokeWidth); +} +} // namespace + +KeyboardPanel::KeyboardPanel() +{ + rows_ = { + {{L"`", L"~"}, {L"1", L"!"}, {L"2", L"@"}, {L"3", L"#"}, {L"4", L"$"}, {L"5", L"%"}, + {L"6", L"^"}, {L"7", L"&"}, {L"8", L"*"}, {L"9", L"("}, {L"0", L")"}, {L"-", L"_"}, + {L"=", L"+"}, {L"Backspace", L"", 1.9f, VK_BACK}}, + {{L"Tab", L"", 1.5f, VK_TAB}, {L"q", L"Q"}, {L"w", L"W"}, {L"e", L"E"}, {L"r", L"R"}, + {L"t", L"T"}, {L"y", L"Y"}, {L"u", L"U"}, {L"i", L"I"}, {L"o", L"O"}, {L"p", L"P"}, + {L"[", L"{"}, {L"]", L"}"}, {L"\\", L"|", 1.4f}}, + {{L"Caps Lock", L"", 1.85f, VK_CAPITAL, {}, true}, {L"a", L"A"}, {L"s", L"S"}, {L"d", L"D"}, + {L"f", L"F"}, {L"g", L"G"}, {L"h", L"H"}, {L"j", L"J"}, {L"k", L"K"}, {L"l", L"L"}, + {L";", L":"}, {L"'", L"\""}, {L"Enter", L"", 2.0f, VK_RETURN}}, + {{L"Shift", L"", 2.35f, VK_SHIFT, {}, true}, {L"z", L"Z"}, {L"x", L"X"}, {L"c", L"C"}, + {L"v", L"V"}, {L"b", L"B"}, {L"n", L"N"}, {L"m", L"M"}, {L",", L"<"}, {L".", L">"}, + {L"/", L"?"}, {L"Shift", L"", 2.15f, VK_SHIFT, {}, true}}, + {{L"Ctrl", L"", 1.25f, VK_CONTROL, {}, true}, {L"Win", L"", 1.25f, VK_LWIN, {}, true}, + {L"Alt", L"", 1.25f, VK_MENU, {}, true}, {L"Space", L" ", 6.7f}, + {L"Alt", L"", 1.25f, VK_MENU, {}, true}, {L"Win", L"", 1.25f, VK_LWIN, {}, true}, + {L"Del", L"", 1.25f, VK_DELETE}, {L"Ctrl", L"", 1.25f, VK_CONTROL, {}, true}}, + }; +} + +SizeF KeyboardPanel::Measure(const SizeF &availableSize) { return availableSize; } + +void KeyboardPanel::Arrange(const RectF &finalRect) +{ + bounds_ = finalRect; + closeRect_ = {bounds_.x + bounds_.width - 34.0f, bounds_.y + 3.0f, 28.0f, 22.0f}; + RebuildLayout(); +} + +void KeyboardPanel::RebuildLayout() +{ + const float contentTop = bounds_.y + kHeaderHeight; + const float availableHeight = std::max(bounds_.height - kHeaderHeight - kOuterPadding, 1.0f); + const float rowHeight = (availableHeight - kGap * static_cast(rows_.size() - 1)) / static_cast(rows_.size()); + for (size_t rowIndex = 0; rowIndex < rows_.size(); ++rowIndex) + { + auto &row = rows_[rowIndex]; + float totalWeight = 0.0f; + for (const auto &key : row) totalWeight += key.weight; + const float availableWidth = bounds_.width - kOuterPadding * 2.0f - kGap * static_cast(row.size() - 1); + const float unit = availableWidth / totalWeight; + float x = bounds_.x + kOuterPadding; + const float y = contentTop + static_cast(rowIndex) * (rowHeight + kGap); + for (auto &key : row) + { + key.rect = {x, y, unit * key.weight, rowHeight}; + x += key.rect.width + kGap; + } + } +} + +void KeyboardPanel::Render(DeviceResources &resources) +{ + FillRounded(resources, bounds_, D2D1::ColorF(0x17181D), 8.0f); + DrawLabel(resources, L"Touch keyboard", {bounds_.x + 10.0f, bounds_.y, 160.0f, kHeaderHeight}, 12.0f, + D2D1::ColorF(0xD7D8DD), DWRITE_TEXT_ALIGNMENT_LEADING); + if (closeHovered_ || closePressed_) FillRounded(resources, closeRect_, D2D1::ColorF(0x3B3D46), 4.0f); + DrawCloseIcon(resources, closeRect_, D2D1::ColorF(0xF5F5F7)); + + size_t flatIndex = 0; + for (const auto &row : rows_) + { + for (const auto &key : row) + { + const bool active = IsKeyActive(key); + const bool hovered = flatIndex == hoveredKey_; + const bool pressed = flatIndex == pressedKey_; + const D2D1_COLOR_F fill = pressed ? D2D1::ColorF(0x666A77) + : (active ? D2D1::ColorF(0x535866) + : (hovered ? D2D1::ColorF(0x41434D) : D2D1::ColorF(0x2B2D34))); + FillRounded(resources, key.rect, fill, 5.0f); + const bool characterKey = key.normal.size() == 1 && key.normal != L" "; + const std::wstring label = characterKey && shiftActive_ && !key.shifted.empty() ? key.shifted : key.normal; + DrawLabel(resources, label, key.rect, characterKey ? 15.0f : 12.0f, D2D1::ColorF(0xF1F1F3)); + ++flatIndex; + } + } +} + +bool KeyboardPanel::HitTest(const PointF &point) const { return Contains(bounds_, point); } + +size_t KeyboardPanel::HitKey(const PointF &point) const +{ + size_t index = 0; + for (const auto &row : rows_) + { + for (const auto &key : row) + { + if (Contains(key.rect, point)) return index; + ++index; + } + } + return kInvalidIndex; +} + +bool KeyboardPanel::OnMouseDown(const POINT &point, WPARAM) +{ + if (!window_) return false; + const PointF dip = window_->ClientPixelsToDips(point); + closePressed_ = Contains(closeRect_, dip); + pressedKey_ = closePressed_ ? kInvalidIndex : HitKey(dip); + InvalidateVisual(); + return true; +} + +bool KeyboardPanel::OnMouseUp(const POINT &point, WPARAM) +{ + if (!window_) return false; + const PointF dip = window_->ClientPixelsToDips(point); + const bool close = closePressed_ && Contains(closeRect_, dip); + const size_t hit = HitKey(dip); + if (pressedKey_ != kInvalidIndex && hit == pressedKey_) ActivateKey(hit); + pressedKey_ = kInvalidIndex; + closePressed_ = false; + InvalidateVisual(); + if (close) PostMessageW(window_->GetHandle(), WM_CLOSE, 0, 0); + return true; +} + +bool KeyboardPanel::OnMouseMove(const POINT &point, WPARAM) +{ + if (!window_) return false; + const PointF dip = window_->ClientPixelsToDips(point); + const bool close = Contains(closeRect_, dip); + const size_t key = close ? kInvalidIndex : HitKey(dip); + if (closeHovered_ != close || hoveredKey_ != key) + { + closeHovered_ = close; + hoveredKey_ = key; + InvalidateVisual(); + } + return true; +} + +void KeyboardPanel::OnMouseLeave() +{ + hoveredKey_ = kInvalidIndex; + closeHovered_ = false; + InvalidateVisual(); +} + +bool KeyboardPanel::IsKeyActive(const Key &key) const +{ + if (key.virtualKey == VK_SHIFT) return shiftActive_; + if (key.virtualKey == VK_CAPITAL) return capsActive_; + if (key.virtualKey == VK_CONTROL) return ctrlActive_; + if (key.virtualKey == VK_MENU) return altActive_; + if (key.virtualKey == VK_LWIN) return winActive_; + return false; +} + +void KeyboardPanel::ActivateKey(size_t target) +{ + size_t index = 0; + for (auto &row : rows_) + { + for (auto &key : row) + { + if (index++ != target) continue; + if (key.virtualKey == VK_SHIFT) shiftActive_ = !shiftActive_; + else if (key.virtualKey == VK_CAPITAL) capsActive_ = !capsActive_; + else if (key.virtualKey == VK_CONTROL) ctrlActive_ = !ctrlActive_; + else if (key.virtualKey == VK_MENU) altActive_ = !altActive_; + else if (key.virtualKey == VK_LWIN) winActive_ = !winActive_; + else if (key.virtualKey) SendVirtualKey(key.virtualKey); + else + { + std::wstring text = shiftActive_ && !key.shifted.empty() ? key.shifted : key.normal; + if (key.normal.size() == 1 && std::iswalpha(key.normal[0]) && (capsActive_ != shiftActive_)) + text[0] = std::towupper(text[0]); + SendText(text); + if (shiftActive_) shiftActive_ = false; + } + return; + } + } +} + +void KeyboardPanel::SendVirtualKey(WORD virtualKey) +{ + INPUT inputs[2] = {}; + inputs[0].type = INPUT_KEYBOARD; + inputs[0].ki.wVk = virtualKey; + inputs[1] = inputs[0]; + inputs[1].ki.dwFlags = KEYEVENTF_KEYUP; + SendInput(2, inputs, sizeof(INPUT)); +} + +void KeyboardPanel::SendText(const std::wstring &text) +{ + for (wchar_t ch : text) + { + INPUT inputs[2] = {}; + inputs[0].type = INPUT_KEYBOARD; + inputs[0].ki.wScan = ch; + inputs[0].ki.dwFlags = KEYEVENTF_UNICODE; + inputs[1] = inputs[0]; + inputs[1].ki.dwFlags = KEYEVENTF_UNICODE | KEYEVENTF_KEYUP; + SendInput(2, inputs, sizeof(INPUT)); + } +} + +HCURSOR KeyboardPanel::GetCursor() const { return LoadCursor(nullptr, IDC_ARROW); } +} // namespace msimeui diff --git a/ui/demos/msimeui-keyboard-demo/KeyboardPanel.h b/ui/demos/msimeui-keyboard-demo/KeyboardPanel.h new file mode 100644 index 00000000..26293a38 --- /dev/null +++ b/ui/demos/msimeui-keyboard-demo/KeyboardPanel.h @@ -0,0 +1,55 @@ +#pragma once + +#include "msimeui/Layout.h" + +#include +#include + +namespace msimeui +{ +class KeyboardPanel final : public Visual +{ + public: + KeyboardPanel(); + + SizeF Measure(const SizeF &availableSize) override; + void Arrange(const RectF &finalRect) override; + void Render(DeviceResources &deviceResources) override; + bool HitTest(const PointF &point) const override; + bool OnMouseDown(const POINT &point, WPARAM keyState) override; + bool OnMouseUp(const POINT &point, WPARAM keyState) override; + bool OnMouseMove(const POINT &point, WPARAM keyState) override; + void OnMouseLeave() override; + HCURSOR GetCursor() const override; + + private: + struct Key + { + std::wstring normal; + std::wstring shifted; + float weight = 1.0f; + WORD virtualKey = 0; + RectF rect = {}; + bool modifier = false; + }; + + size_t HitKey(const PointF &point) const; + void RebuildLayout(); + void ActivateKey(size_t index); + void SendVirtualKey(WORD virtualKey); + void SendText(const std::wstring &text); + bool IsKeyActive(const Key &key) const; + + std::vector> rows_; + RectF closeRect_ = {}; + size_t hoveredKey_ = static_cast(-1); + size_t pressedKey_ = static_cast(-1); + bool closeHovered_ = false; + bool closePressed_ = false; + bool shiftActive_ = false; + bool capsActive_ = false; + bool ctrlActive_ = false; + bool altActive_ = false; + bool winActive_ = false; +}; +} // namespace msimeui diff --git a/ui/demos/msimeui-keyboard-demo/main.cpp b/ui/demos/msimeui-keyboard-demo/main.cpp new file mode 100644 index 00000000..f7982261 --- /dev/null +++ b/ui/demos/msimeui-keyboard-demo/main.cpp @@ -0,0 +1,37 @@ +#include "KeyboardPanel.h" + +#include "msimeui/Application.h" +#include "msimeui/Scene.h" +#include "msimeui/Theme.h" +#include "msimeui/Window.h" + +#include + +int WINAPI wWinMain(HINSTANCE, HINSTANCE, PWSTR, int nCmdShow) +{ + if (!msimeui::Application::Initialize()) return -1; + + msimeui::Theme theme = msimeui::ThemeManager::GetCurrent(); + theme.windowBackground = D2D1::ColorF(0x17181D); + theme.surface = D2D1::ColorF(0x17181D); + theme.textPrimary = D2D1::ColorF(0xF1F1F3); + theme.textSecondary = D2D1::ColorF(0xAEB0B7); + msimeui::ThemeManager::SetCurrent(std::move(theme)); + + msimeui::Window window(L"msimeui.KeyboardDemo", L"Dark touch keyboard", 1100, 400); + window.SetWindowStyle(WS_POPUP, WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE); + window.SetDragRegionHeight(28.0f); + window.SetRoundedCorners(true); + if (!window.Create()) + { + msimeui::Application::Shutdown(); + return -1; + } + + auto scene = std::make_unique(); + scene->SetRoot(std::make_shared()); + window.SetScene(std::move(scene)); + const int result = window.Run(nCmdShow); + msimeui::Application::Shutdown(); + return result; +} diff --git a/ui/demos/msimeui-keyboard-demo/msimeui-keyboard-demo.rc b/ui/demos/msimeui-keyboard-demo/msimeui-keyboard-demo.rc new file mode 100644 index 00000000..41ae81e8 --- /dev/null +++ b/ui/demos/msimeui-keyboard-demo/msimeui-keyboard-demo.rc @@ -0,0 +1,3 @@ +#include "resource.h" + +IDI_MSIMEUI_ICON ICON "../../assets/msimeui.ico" diff --git a/ui/demos/msimeui-keyboard-demo/resource.h b/ui/demos/msimeui-keyboard-demo/resource.h new file mode 100644 index 00000000..b132b872 --- /dev/null +++ b/ui/demos/msimeui-keyboard-demo/resource.h @@ -0,0 +1,3 @@ +#pragma once + +#define IDI_MSIMEUI_ICON 101 diff --git a/ui/include/msimeui/Window.h b/ui/include/msimeui/Window.h index af170757..559dcd54 100644 --- a/ui/include/msimeui/Window.h +++ b/ui/include/msimeui/Window.h @@ -59,6 +59,7 @@ class Window Visual *focusedVisual_ = nullptr; Visual *hoveredVisual_ = nullptr; Visual *capturedVisual_ = nullptr; + bool mouseLeaveTracking_ = false; DWORD windowStyle_ = WS_OVERLAPPEDWINDOW; DWORD extendedWindowStyle_ = 0; float dragRegionHeight_ = 0.0f; diff --git a/ui/scripts/build-keyboard-demo.ps1 b/ui/scripts/build-keyboard-demo.ps1 new file mode 100644 index 00000000..369662ab --- /dev/null +++ b/ui/scripts/build-keyboard-demo.ps1 @@ -0,0 +1,26 @@ +[CmdletBinding()] +param( + [ValidateSet("Debug", "Release")] + [string]$Configuration = "Debug", + [switch]$Fresh +) + +$ErrorActionPreference = "Stop" +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$repoRoot = Split-Path -Parent $scriptDir +$configurePreset = if ($Configuration -eq "Release") { "vcpkg-release" } else { "vcpkg-debug" } +$buildPreset = if ($Configuration -eq "Release") { "release" } else { "debug" } + +Push-Location $repoRoot +try { + $configureArguments = @("--preset", $configurePreset) + if ($Fresh) { $configureArguments += "--fresh" } + & cmake @configureArguments + if ($LASTEXITCODE -ne 0) { throw "CMake configure failed with exit code $LASTEXITCODE." } + + & cmake --build --preset $buildPreset --target msimeui-keyboard-demo + if ($LASTEXITCODE -ne 0) { throw "Keyboard demo build failed with exit code $LASTEXITCODE." } +} +finally { + Pop-Location +} diff --git a/ui/scripts/launch-keyboard-demo.ps1 b/ui/scripts/launch-keyboard-demo.ps1 new file mode 100644 index 00000000..e1daaf86 --- /dev/null +++ b/ui/scripts/launch-keyboard-demo.ps1 @@ -0,0 +1,21 @@ +[CmdletBinding()] +param( + [ValidateSet("Debug", "Release")] + [string]$Configuration = "Debug", + [switch]$Build +) + +$ErrorActionPreference = "Stop" +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$repoRoot = Split-Path -Parent $scriptDir + +if ($Build) { + & (Join-Path $scriptDir "build-keyboard-demo.ps1") -Configuration $Configuration +} + +$demoPath = Join-Path $repoRoot "build/bin/$Configuration/msimeui-keyboard-demo.exe" +if (-not (Test-Path -LiteralPath $demoPath)) { + throw "Keyboard demo executable not found: $demoPath. Run build-keyboard-demo.ps1 first or pass -Build." +} + +Start-Process -FilePath $demoPath -WorkingDirectory $repoRoot diff --git a/ui/src/Window.cpp b/ui/src/Window.cpp index 9df766fb..fcaf4530 100644 --- a/ui/src/Window.cpp +++ b/ui/src/Window.cpp @@ -325,6 +325,18 @@ LRESULT Window::HandleMessage(UINT message, WPARAM wParam, LPARAM lParam) case WM_MOUSEMOVE: { + if (!mouseLeaveTracking_) + { + TRACKMOUSEEVENT tracking = {}; + tracking.cbSize = sizeof(tracking); + tracking.dwFlags = TME_LEAVE; + tracking.hwndTrack = hwnd_; + if (TrackMouseEvent(&tracking)) + { + mouseLeaveTracking_ = true; + } + } + const POINT point = {GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)}; UpdateCursorForClientPoint(point); Visual *hoverTarget = nullptr; @@ -360,6 +372,16 @@ LRESULT Window::HandleMessage(UINT message, WPARAM wParam, LPARAM lParam) return 0; } + case WM_MOUSELEAVE: + mouseLeaveTracking_ = false; + if (hoveredVisual_) + { + hoveredVisual_->OnMouseLeave(); + hoveredVisual_ = nullptr; + } + SetCursor(LoadCursor(nullptr, IDC_ARROW)); + return 0; + case WM_MOUSEWHEEL: if (OnMouseWheel(wParam, lParam)) {