Files
PowerToys/src/modules/MouseUtils/MousePointerCrosshairs/InclusiveCrosshairs.cpp
Mike Hall df08d98a81 Implement "Gliding cursor" accessibility feature (#41221)
## Summary of the Pull Request
Added '[Gliding
Cursor](https://github.com/microsoft/PowerToys/issues/37097)'
functionality to Mouse Pointer Crosshairs, this enables a single
hotkey/Microsoft Adaptive Hub + button to control cursor movement and
clicking. This is implemented as an extension to the existing Mouse
Pointer Crosshairs module.

Testing has been manual, ensuring that the existing Mouse Pointer
Crosshairs functionality is unchanged, and that the new Gliding Cursor
functionality works alongside Mouse Pointer Crosshairs.


![FlowPointer2](https://github.com/user-attachments/assets/ede40fe5-d749-45d1-bd8d-627dda2927a3)

<img width="857" height="438" alt="image"
src="https://github.com/user-attachments/assets/b9e7ee72-dfeb-4d20-93a5-a34e8b10d703"
/>


To test this functionality:
- Open Mouse Crosshair settings and make sure the feature is enabled.
- Press the shortcut to start the gliding cursor — a vertical line
appears.
- Press the shortcut again to slow the vertical line.
- Press once more to fix the vertical line; a horizontal line begins
moving.
- Press again to slow the horizontal line.
- When the lines meet at your target, press the shortcut to perform the
click.

## PR Checklist

- [x] Closes: #37097
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [x] **Tests:** Added/updated and all pass
- [x] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

## Detailed Description of the Pull Request / Additional comments
The PR includes these changes:
* Updated Mouse Pointer Crosshairs XAML to include a new hotkey to start
the gliding cursor experience
* Added two sliders for fast/slow cursor movement
* mapped the new hotkey/XAML sliders through to the existing
MousePointerHotkeys project, dllmain.cpp
* Added a 10ms tick for Gliding cursor for crosshairs/cursor movement
* Added state for gliding functionality - horiz fast, horiz slow, vert
fast, vert slow, click
* added gates around the existing mouse movement hook to prevent mouse
movement when gliding


## Validation Steps Performed
Manual testing has been completed on several PCs to confirm the
following:
* Existing Mouse Pointer Crosshairs functionality is unchanged
* Gliding cursor settings are persisted/used by the gliding cursor code
* Gliding cursor restores Mouse Pointer Crosshairs state after the final
click has completed.

---------

Signed-off-by: Shawn Yuan <shuaiyuan@microsoft.com>
Co-authored-by: Niels Laute <niels.laute@live.nl>
Co-authored-by: Shawn Yuan <shuaiyuan@microsoft.com>
2025-08-21 13:53:20 +08:00

663 lines
25 KiB
C++

// InclusiveCrosshairs.cpp : Defines the entry point for the application.
//
#include "pch.h"
#include "InclusiveCrosshairs.h"
#include "trace.h"
#ifdef COMPOSITION
namespace winrt
{
using namespace winrt::Windows::System;
using namespace winrt::Windows::UI::Composition;
}
namespace ABI
{
using namespace ABI::Windows::System;
using namespace ABI::Windows::UI::Composition::Desktop;
}
#endif
struct InclusiveCrosshairs
{
bool MyRegisterClass(HINSTANCE hInstance);
static InclusiveCrosshairs* instance;
void Terminate();
void SwitchActivationMode();
void ApplySettings(InclusiveCrosshairsSettings& settings, bool applyToRuntimeObjects);
public:
// Allow external callers to request a position update (thread-safe enqueue)
static void RequestUpdatePosition()
{
if (instance != nullptr)
{
auto dispatcherQueue = instance->m_dispatcherQueueController.DispatcherQueue();
dispatcherQueue.TryEnqueue([]() {
if (instance != nullptr)
{
instance->UpdateCrosshairsPosition();
}
});
}
}
static void EnsureOn()
{
if (instance != nullptr)
{
auto dispatcherQueue = instance->m_dispatcherQueueController.DispatcherQueue();
dispatcherQueue.TryEnqueue([]() {
if (instance != nullptr && !instance->m_drawing)
{
instance->StartDrawing();
}
});
}
}
static void EnsureOff()
{
if (instance != nullptr)
{
auto dispatcherQueue = instance->m_dispatcherQueueController.DispatcherQueue();
dispatcherQueue.TryEnqueue([]() {
if (instance != nullptr && instance->m_drawing)
{
instance->StopDrawing();
}
});
}
}
static void SetExternalControl(bool enabled)
{
if (instance != nullptr)
{
auto dispatcherQueue = instance->m_dispatcherQueueController.DispatcherQueue();
dispatcherQueue.TryEnqueue([enabled]() {
if (instance != nullptr)
{
instance->m_externalControl = enabled;
if (enabled && instance->m_mouseHook)
{
UnhookWindowsHookEx(instance->m_mouseHook);
instance->m_mouseHook = NULL;
}
else if (!enabled && instance->m_drawing && !instance->m_mouseHook)
{
instance->m_mouseHook = SetWindowsHookEx(WH_MOUSE_LL, MouseHookProc, instance->m_hinstance, 0);
}
}
});
}
}
private:
enum class MouseButton
{
Left,
Right
};
void DestroyInclusiveCrosshairs();
static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) noexcept;
void StartDrawing();
void StopDrawing();
bool CreateInclusiveCrosshairs();
void UpdateCrosshairsPosition();
HHOOK m_mouseHook = NULL;
static LRESULT CALLBACK MouseHookProc(int nCode, WPARAM wParam, LPARAM lParam) noexcept;
static constexpr auto m_className = L"MousePointerCrosshairs";
static constexpr auto m_windowTitle = L"PowerToys Mouse Pointer Crosshairs";
static constexpr DWORD AUTO_HIDE_TIMER_ID = 101;
HWND m_hwndOwner = NULL;
HWND m_hwnd = NULL;
HINSTANCE m_hinstance = NULL;
static constexpr DWORD WM_SWITCH_ACTIVATION_MODE = WM_APP;
winrt::DispatcherQueueController m_dispatcherQueueController{ nullptr };
winrt::Compositor m_compositor{ nullptr };
winrt::Desktop::DesktopWindowTarget m_target{ nullptr };
winrt::ContainerVisual m_root{ nullptr };
winrt::LayerVisual m_crosshairs_border_layer{ nullptr };
winrt::LayerVisual m_crosshairs_layer{ nullptr };
winrt::SpriteVisual m_left_crosshairs_border{ nullptr };
winrt::SpriteVisual m_left_crosshairs{ nullptr };
winrt::SpriteVisual m_right_crosshairs_border{ nullptr };
winrt::SpriteVisual m_right_crosshairs{ nullptr };
winrt::SpriteVisual m_top_crosshairs_border{ nullptr };
winrt::SpriteVisual m_top_crosshairs{ nullptr };
winrt::SpriteVisual m_bottom_crosshairs_border{ nullptr };
winrt::SpriteVisual m_bottom_crosshairs{ nullptr };
bool m_drawing = false;
bool m_destroyed = false;
bool m_hiddenCursor = false;
bool m_externalControl = false;
void SetAutoHideTimer() noexcept;
// Configurable Settings
winrt::Windows::UI::Color m_crosshairs_border_color = INCLUSIVE_MOUSE_DEFAULT_CROSSHAIRS_BORDER_COLOR;
winrt::Windows::UI::Color m_crosshairs_color = INCLUSIVE_MOUSE_DEFAULT_CROSSHAIRS_COLOR;
int m_crosshairs_radius = INCLUSIVE_MOUSE_DEFAULT_CROSSHAIRS_RADIUS;
int m_crosshairs_thickness = INCLUSIVE_MOUSE_DEFAULT_CROSSHAIRS_THICKNESS;
int m_crosshairs_border_size = INCLUSIVE_MOUSE_DEFAULT_CROSSHAIRS_BORDER_SIZE;
bool m_crosshairs_is_fixed_length_enabled = INCLUSIVE_MOUSE_DEFAULT_CROSSHAIRS_IS_FIXED_LENGTH_ENABLED;
int m_crosshairs_fixed_length = INCLUSIVE_MOUSE_DEFAULT_CROSSHAIRS_FIXED_LENGTH;
float m_crosshairs_opacity = max(0.f, min(1.f, (float)INCLUSIVE_MOUSE_DEFAULT_CROSSHAIRS_OPACITY / 100.0f));
bool m_crosshairs_auto_hide = INCLUSIVE_MOUSE_DEFAULT_AUTO_HIDE;
};
InclusiveCrosshairs* InclusiveCrosshairs::instance = nullptr;
bool InclusiveCrosshairs::CreateInclusiveCrosshairs()
{
try
{
// We need a dispatcher queue.
DispatcherQueueOptions options = {
sizeof(options),
DQTYPE_THREAD_CURRENT,
DQTAT_COM_ASTA,
};
ABI::IDispatcherQueueController* controller;
winrt::check_hresult(CreateDispatcherQueueController(options, &controller));
*winrt::put_abi(m_dispatcherQueueController) = controller;
// Create the compositor for our window.
m_compositor = winrt::Compositor();
ABI::IDesktopWindowTarget* target;
winrt::check_hresult(m_compositor.as<ABI::ICompositorDesktopInterop>()->CreateDesktopWindowTarget(m_hwnd, false, &target));
*winrt::put_abi(m_target) = target;
// Our composition tree:
//
// [root] ContainerVisual
// \ [crosshairs border layer] LayerVisual
// \ [crosshairs border sprites]
// [crosshairs layer] LayerVisual
// \ [crosshairs sprites]
m_root = m_compositor.CreateContainerVisual();
m_root.RelativeSizeAdjustment({ 1.0f, 1.0f });
m_target.Root(m_root);
m_root.Opacity(m_crosshairs_opacity);
m_crosshairs_border_layer = m_compositor.CreateLayerVisual();
m_crosshairs_border_layer.RelativeSizeAdjustment({ 1.0f, 1.0f });
m_root.Children().InsertAtTop(m_crosshairs_border_layer);
m_crosshairs_border_layer.Opacity(1.0f);
m_crosshairs_layer = m_compositor.CreateLayerVisual();
m_crosshairs_layer.RelativeSizeAdjustment({ 1.0f, 1.0f });
// Create the crosshairs sprites.
m_left_crosshairs_border = m_compositor.CreateSpriteVisual();
m_left_crosshairs_border.AnchorPoint({ 1.0f, 0.5f });
m_left_crosshairs_border.Brush(m_compositor.CreateColorBrush(m_crosshairs_border_color));
m_crosshairs_border_layer.Children().InsertAtTop(m_left_crosshairs_border);
m_left_crosshairs = m_compositor.CreateSpriteVisual();
m_left_crosshairs.AnchorPoint({ 1.0f, 0.5f });
m_left_crosshairs.Brush(m_compositor.CreateColorBrush(m_crosshairs_color));
m_crosshairs_layer.Children().InsertAtTop(m_left_crosshairs);
m_right_crosshairs_border = m_compositor.CreateSpriteVisual();
m_right_crosshairs_border.AnchorPoint({ 0.0f, 0.5f });
m_right_crosshairs_border.Brush(m_compositor.CreateColorBrush(m_crosshairs_border_color));
m_crosshairs_border_layer.Children().InsertAtTop(m_right_crosshairs_border);
m_right_crosshairs = m_compositor.CreateSpriteVisual();
m_right_crosshairs.AnchorPoint({ 0.0f, 0.5f });
m_right_crosshairs.Brush(m_compositor.CreateColorBrush(m_crosshairs_color));
m_crosshairs_layer.Children().InsertAtTop(m_right_crosshairs);
m_top_crosshairs_border = m_compositor.CreateSpriteVisual();
m_top_crosshairs_border.AnchorPoint({ 0.5f, 1.0f });
m_top_crosshairs_border.Brush(m_compositor.CreateColorBrush(m_crosshairs_border_color));
m_crosshairs_border_layer.Children().InsertAtTop(m_top_crosshairs_border);
m_top_crosshairs = m_compositor.CreateSpriteVisual();
m_top_crosshairs.AnchorPoint({ 0.5f, 1.0f });
m_top_crosshairs.Brush(m_compositor.CreateColorBrush(m_crosshairs_color));
m_crosshairs_layer.Children().InsertAtTop(m_top_crosshairs);
m_bottom_crosshairs_border = m_compositor.CreateSpriteVisual();
m_bottom_crosshairs_border.AnchorPoint({ 0.5f, 0.0f });
m_bottom_crosshairs_border.Brush(m_compositor.CreateColorBrush(m_crosshairs_border_color));
m_crosshairs_border_layer.Children().InsertAtTop(m_bottom_crosshairs_border);
m_bottom_crosshairs = m_compositor.CreateSpriteVisual();
m_bottom_crosshairs.AnchorPoint({ 0.5f, 0.0f });
m_bottom_crosshairs.Brush(m_compositor.CreateColorBrush(m_crosshairs_color));
m_crosshairs_layer.Children().InsertAtTop(m_bottom_crosshairs);
m_crosshairs_border_layer.Children().InsertAtTop(m_crosshairs_layer);
m_crosshairs_layer.Opacity(1.0f);
UpdateCrosshairsPosition();
return true;
}
catch (...)
{
return false;
}
}
void InclusiveCrosshairs::UpdateCrosshairsPosition()
{
POINT ptCursor;
// HACK: Draw with 1 pixel off. Otherwise, Windows glitches the task bar transparency when a transparent window fill the whole screen.
SetWindowPos(m_hwnd, HWND_TOPMOST, GetSystemMetrics(SM_XVIRTUALSCREEN) + 1, GetSystemMetrics(SM_YVIRTUALSCREEN) + 1, GetSystemMetrics(SM_CXVIRTUALSCREEN) - 2, GetSystemMetrics(SM_CYVIRTUALSCREEN) - 2, 0);
GetCursorPos(&ptCursor);
HMONITOR cursorMonitor = MonitorFromPoint(ptCursor, MONITOR_DEFAULTTONEAREST);
if (cursorMonitor == NULL)
{
return;
}
MONITORINFO monitorInfo;
monitorInfo.cbSize = sizeof(monitorInfo);
if (!GetMonitorInfo(cursorMonitor, &monitorInfo))
{
return;
}
POINT ptMonitorUpperLeft;
ptMonitorUpperLeft.x = monitorInfo.rcMonitor.left;
ptMonitorUpperLeft.y = monitorInfo.rcMonitor.top;
POINT ptMonitorBottomRight;
ptMonitorBottomRight.x = monitorInfo.rcMonitor.right;
ptMonitorBottomRight.y = monitorInfo.rcMonitor.bottom;
// Convert everything to client coordinates.
ScreenToClient(m_hwnd, &ptCursor);
ScreenToClient(m_hwnd, &ptMonitorUpperLeft);
ScreenToClient(m_hwnd, &ptMonitorBottomRight);
// Crosshair position should receive a minor adjustment for odd values to prevent anti-aliasing due to half pixels, while still looking like it's centered around the mouse pointer.
float halfPixelAdjustment = m_crosshairs_thickness % 2 == 1 ? 0.5f : 0.0f;
float borderSizePadding = m_crosshairs_border_size * 2.f;
{
float leftCrosshairsFullScreenLength = ptCursor.x - ptMonitorUpperLeft.x - m_crosshairs_radius + halfPixelAdjustment * 2.f;
float leftCrosshairsLength = m_crosshairs_is_fixed_length_enabled ? m_crosshairs_fixed_length : leftCrosshairsFullScreenLength;
float leftCrosshairsBorderLength = m_crosshairs_is_fixed_length_enabled ? m_crosshairs_fixed_length + borderSizePadding : leftCrosshairsFullScreenLength + m_crosshairs_border_size;
m_left_crosshairs_border.Offset({ ptCursor.x - m_crosshairs_radius + m_crosshairs_border_size + halfPixelAdjustment * 2.f, ptCursor.y + halfPixelAdjustment, .0f });
m_left_crosshairs_border.Size({ leftCrosshairsBorderLength, m_crosshairs_thickness + borderSizePadding });
m_left_crosshairs.Offset({ ptCursor.x - m_crosshairs_radius + halfPixelAdjustment * 2.f, ptCursor.y + halfPixelAdjustment, .0f });
m_left_crosshairs.Size({ leftCrosshairsLength, static_cast<float>(m_crosshairs_thickness) });
}
{
float rightCrosshairsFullScreenLength = static_cast<float>(ptMonitorBottomRight.x) - ptCursor.x - m_crosshairs_radius;
float rightCrosshairsLength = m_crosshairs_is_fixed_length_enabled ? m_crosshairs_fixed_length : rightCrosshairsFullScreenLength;
float rightCrosshairsBorderLength = m_crosshairs_is_fixed_length_enabled ? m_crosshairs_fixed_length + borderSizePadding : rightCrosshairsFullScreenLength + m_crosshairs_border_size;
m_right_crosshairs_border.Offset({ static_cast<float>(ptCursor.x) + m_crosshairs_radius - m_crosshairs_border_size, ptCursor.y + halfPixelAdjustment, .0f });
m_right_crosshairs_border.Size({ rightCrosshairsBorderLength, m_crosshairs_thickness + borderSizePadding });
m_right_crosshairs.Offset({ static_cast<float>(ptCursor.x) + m_crosshairs_radius, ptCursor.y + halfPixelAdjustment, .0f });
m_right_crosshairs.Size({ rightCrosshairsLength, static_cast<float>(m_crosshairs_thickness) });
}
{
float topCrosshairsFullScreenLength = ptCursor.y - ptMonitorUpperLeft.y - m_crosshairs_radius + halfPixelAdjustment * 2.f;
float topCrosshairsLength = m_crosshairs_is_fixed_length_enabled ? m_crosshairs_fixed_length : topCrosshairsFullScreenLength;
float topCrosshairsBorderLength = m_crosshairs_is_fixed_length_enabled ? m_crosshairs_fixed_length + borderSizePadding : topCrosshairsFullScreenLength + m_crosshairs_border_size;
m_top_crosshairs_border.Offset({ ptCursor.x + halfPixelAdjustment, ptCursor.y - m_crosshairs_radius + m_crosshairs_border_size + halfPixelAdjustment * 2.f, .0f });
m_top_crosshairs_border.Size({ m_crosshairs_thickness + borderSizePadding, topCrosshairsBorderLength });
m_top_crosshairs.Offset({ ptCursor.x + halfPixelAdjustment, ptCursor.y - m_crosshairs_radius + halfPixelAdjustment * 2.f, .0f });
m_top_crosshairs.Size({ static_cast<float>(m_crosshairs_thickness), topCrosshairsLength });
}
{
float bottomCrosshairsFullScreenLength = static_cast<float>(ptMonitorBottomRight.y) - ptCursor.y - m_crosshairs_radius;
float bottomCrosshairsLength = m_crosshairs_is_fixed_length_enabled ? m_crosshairs_fixed_length : bottomCrosshairsFullScreenLength;
float bottomCrosshairsBorderLength = m_crosshairs_is_fixed_length_enabled ? m_crosshairs_fixed_length + borderSizePadding : bottomCrosshairsFullScreenLength + m_crosshairs_border_size;
m_bottom_crosshairs_border.Offset({ ptCursor.x + halfPixelAdjustment, static_cast<float>(ptCursor.y) + m_crosshairs_radius - m_crosshairs_border_size, .0f });
m_bottom_crosshairs_border.Size({ m_crosshairs_thickness + borderSizePadding, bottomCrosshairsBorderLength });
m_bottom_crosshairs.Offset({ ptCursor.x + halfPixelAdjustment, static_cast<float>(ptCursor.y) + m_crosshairs_radius, .0f });
m_bottom_crosshairs.Size({ static_cast<float>(m_crosshairs_thickness), bottomCrosshairsLength });
}
}
LRESULT CALLBACK InclusiveCrosshairs::MouseHookProc(int nCode, WPARAM wParam, LPARAM lParam) noexcept
{
if (nCode >= 0)
{
MSLLHOOKSTRUCT* hookData = reinterpret_cast<MSLLHOOKSTRUCT*>(lParam);
if (instance && !instance->m_externalControl)
{
if (wParam == WM_MOUSEMOVE)
{
instance->UpdateCrosshairsPosition();
}
}
}
return CallNextHookEx(0, nCode, wParam, lParam);
}
void InclusiveCrosshairs::StartDrawing()
{
Logger::info("Start drawing crosshairs.");
Trace::StartDrawingCrosshairs();
UpdateCrosshairsPosition();
m_hiddenCursor = false;
if (m_crosshairs_auto_hide)
{
CURSORINFO cursorInfo{};
cursorInfo.cbSize = sizeof(cursorInfo);
if (GetCursorInfo(&cursorInfo))
{
m_hiddenCursor = !(cursorInfo.flags & CURSOR_SHOWING);
}
SetAutoHideTimer();
}
if (!m_hiddenCursor)
{
ShowWindow(m_hwnd, SW_SHOWNOACTIVATE);
}
m_drawing = true;
m_mouseHook = SetWindowsHookEx(WH_MOUSE_LL, MouseHookProc, m_hinstance, 0);
}
void InclusiveCrosshairs::StopDrawing()
{
Logger::info("Stop drawing crosshairs.");
m_drawing = false;
ShowWindow(m_hwnd, SW_HIDE);
UnhookWindowsHookEx(m_mouseHook);
m_mouseHook = NULL;
KillTimer(m_hwnd, AUTO_HIDE_TIMER_ID);
}
void InclusiveCrosshairs::SwitchActivationMode()
{
PostMessage(m_hwnd, WM_SWITCH_ACTIVATION_MODE, 0, 0);
}
void InclusiveCrosshairs::ApplySettings(InclusiveCrosshairsSettings& settings, bool applyToRunTimeObjects)
{
m_crosshairs_radius = settings.crosshairsRadius;
m_crosshairs_thickness = settings.crosshairsThickness;
m_crosshairs_color = settings.crosshairsColor;
m_crosshairs_opacity = max(0.f, min(1.f, (float)settings.crosshairsOpacity / 100.0f));
m_crosshairs_border_color = settings.crosshairsBorderColor;
m_crosshairs_border_size = settings.crosshairsBorderSize;
bool autoHideChanged = m_crosshairs_auto_hide != settings.crosshairsAutoHide;
m_crosshairs_auto_hide = settings.crosshairsAutoHide;
m_crosshairs_is_fixed_length_enabled = settings.crosshairsIsFixedLengthEnabled;
m_crosshairs_fixed_length = settings.crosshairsFixedLength;
if (applyToRunTimeObjects)
{
if (autoHideChanged)
{
if (m_crosshairs_auto_hide)
{
SetAutoHideTimer();
}
else
{
KillTimer(m_hwnd, AUTO_HIDE_TIMER_ID);
// Edge case of settings being changed with hidden crosshairs: timer time-out is 1 seconds
if (m_drawing && m_hiddenCursor)
{
instance->m_hiddenCursor = false;
ShowWindow(instance->m_hwnd, SW_SHOWNOACTIVATE);
}
}
}
// Runtime objects already created. Should update in the owner thread.
if (m_dispatcherQueueController == nullptr)
{
Logger::warn("Tried accessing the dispatch queue controller before it was initialized.");
// No dispatcher Queue Controller? Means initialization still hasn't run, so settings will be applied then.
return;
}
auto dispatcherQueue = m_dispatcherQueueController.DispatcherQueue();
InclusiveCrosshairsSettings localSettings = settings;
bool enqueueSucceeded = dispatcherQueue.TryEnqueue([=]() {
if (!m_destroyed)
{
// Apply new settings to runtime composition objects.
m_left_crosshairs.Brush().as<winrt::CompositionColorBrush>().Color(m_crosshairs_color);
m_right_crosshairs.Brush().as<winrt::CompositionColorBrush>().Color(m_crosshairs_color);
m_top_crosshairs.Brush().as<winrt::CompositionColorBrush>().Color(m_crosshairs_color);
m_bottom_crosshairs.Brush().as<winrt::CompositionColorBrush>().Color(m_crosshairs_color);
m_left_crosshairs_border.Brush().as<winrt::CompositionColorBrush>().Color(m_crosshairs_border_color);
m_right_crosshairs_border.Brush().as<winrt::CompositionColorBrush>().Color(m_crosshairs_border_color);
m_top_crosshairs_border.Brush().as<winrt::CompositionColorBrush>().Color(m_crosshairs_border_color);
m_bottom_crosshairs_border.Brush().as<winrt::CompositionColorBrush>().Color(m_crosshairs_border_color);
m_root.Opacity(m_crosshairs_opacity);
UpdateCrosshairsPosition();
}
});
if (!enqueueSucceeded)
{
Logger::error("Couldn't enqueue message to update the crosshairs settings.");
}
}
}
void InclusiveCrosshairs::DestroyInclusiveCrosshairs()
{
StopDrawing();
PostQuitMessage(0);
}
LRESULT CALLBACK InclusiveCrosshairs::WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) noexcept
{
switch (message)
{
case WM_NCCREATE:
instance->m_hwnd = hWnd;
return DefWindowProc(hWnd, message, wParam, lParam);
case WM_CREATE:
return instance->CreateInclusiveCrosshairs() ? 0 : -1;
case WM_NCHITTEST:
return HTTRANSPARENT;
case WM_SWITCH_ACTIVATION_MODE:
if (instance->m_drawing)
{
instance->StopDrawing();
}
else
{
instance->StartDrawing();
}
break;
case WM_DESTROY:
instance->DestroyInclusiveCrosshairs();
break;
case WM_TIMER:
if (wParam == AUTO_HIDE_TIMER_ID && instance->m_drawing)
{
CURSORINFO cursorInfo{};
cursorInfo.cbSize = sizeof(cursorInfo);
if (GetCursorInfo(&cursorInfo))
{
if (cursorInfo.flags & CURSOR_SHOWING)
{
if (instance->m_hiddenCursor)
{
instance->m_hiddenCursor = false;
ShowWindow(instance->m_hwnd, SW_SHOWNOACTIVATE);
}
}
else
{
if (!instance->m_hiddenCursor)
{
instance->m_hiddenCursor = true;
ShowWindow(instance->m_hwnd, SW_HIDE);
}
}
}
}
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
bool InclusiveCrosshairs::MyRegisterClass(HINSTANCE hInstance)
{
WNDCLASS wc{};
m_hinstance = hInstance;
SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
if (!GetClassInfoW(hInstance, m_className, &wc))
{
wc.lpfnWndProc = WndProc;
wc.hInstance = hInstance;
wc.hIcon = LoadIcon(hInstance, IDI_APPLICATION);
wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
wc.hbrBackground = static_cast<HBRUSH>(GetStockObject(NULL_BRUSH));
wc.lpszClassName = m_className;
if (!RegisterClassW(&wc))
{
return false;
}
}
m_hwndOwner = CreateWindow(L"static", nullptr, WS_POPUP, 0, 0, 0, 0, nullptr, nullptr, hInstance, nullptr);
DWORD exStyle = WS_EX_TRANSPARENT | WS_EX_LAYERED | WS_EX_NOREDIRECTIONBITMAP | WS_EX_TOOLWINDOW;
return CreateWindowExW(exStyle, m_className, m_windowTitle, WS_POPUP, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, m_hwndOwner, nullptr, hInstance, nullptr) != nullptr;
}
void InclusiveCrosshairs::Terminate()
{
auto dispatcherQueue = m_dispatcherQueueController.DispatcherQueue();
bool enqueueSucceeded = dispatcherQueue.TryEnqueue([=]() {
m_destroyed = true;
DestroyWindow(m_hwndOwner);
});
if (!enqueueSucceeded)
{
Logger::error("Couldn't enqueue message to destroy the window.");
}
}
void InclusiveCrosshairs::SetAutoHideTimer() noexcept
{
if (SetTimer(m_hwnd, AUTO_HIDE_TIMER_ID, 1000, NULL) == 0)
{
int error = GetLastError();
Logger::trace("Failed to create auto hide timer. Last error: {}", error);
}
}
#pragma region InclusiveCrosshairs_API
void InclusiveCrosshairsApplySettings(InclusiveCrosshairsSettings& settings)
{
if (InclusiveCrosshairs::instance != nullptr)
{
Logger::info("Applying settings.");
InclusiveCrosshairs::instance->ApplySettings(settings, true);
}
}
void InclusiveCrosshairsSwitch()
{
if (InclusiveCrosshairs::instance != nullptr)
{
Logger::info("Switching activation mode.");
InclusiveCrosshairs::instance->SwitchActivationMode();
}
}
void InclusiveCrosshairsDisable()
{
if (InclusiveCrosshairs::instance != nullptr)
{
Logger::info("Terminating the crosshairs instance.");
InclusiveCrosshairs::instance->Terminate();
}
}
bool InclusiveCrosshairsIsEnabled()
{
return (InclusiveCrosshairs::instance != nullptr);
}
void InclusiveCrosshairsRequestUpdatePosition()
{
InclusiveCrosshairs::RequestUpdatePosition();
}
void InclusiveCrosshairsEnsureOn()
{
InclusiveCrosshairs::EnsureOn();
}
void InclusiveCrosshairsEnsureOff()
{
InclusiveCrosshairs::EnsureOff();
}
void InclusiveCrosshairsSetExternalControl(bool enabled)
{
InclusiveCrosshairs::SetExternalControl(enabled);
}
int InclusiveCrosshairsMain(HINSTANCE hInstance, InclusiveCrosshairsSettings& settings)
{
Logger::info("Starting a crosshairs instance.");
if (InclusiveCrosshairs::instance != nullptr)
{
Logger::error("A crosshairs instance was still working when trying to start a new one.");
return 0;
}
// Perform application initialization:
InclusiveCrosshairs crosshairs;
InclusiveCrosshairs::instance = &crosshairs;
crosshairs.ApplySettings(settings, false);
if (!crosshairs.MyRegisterClass(hInstance))
{
Logger::error("Couldn't initialize a crosshairs instance.");
InclusiveCrosshairs::instance = nullptr;
return FALSE;
}
Logger::info("Initialized the crosshairs instance.");
if (settings.autoActivate)
{
crosshairs.SwitchActivationMode();
}
MSG msg;
// Main message loop:
while (GetMessage(&msg, nullptr, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
Logger::info("Crosshairs message loop ended.");
InclusiveCrosshairs::instance = nullptr;
return (int)msg.wParam;
}
#pragma endregion InclusiveCrosshairs_API