Mouse highlighter: support a spotlight mode - inner transparent, out a backdrop (#40043)

<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] **Closes:** #15512 
- [x] **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

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed


https://github.com/user-attachments/assets/0748c526-fcf5-4859-b832-14a413d2cad1
This commit is contained in:
Kai Tao
2025-06-27 14:11:39 +08:00
committed by GitHub
parent 0134823de1
commit 16742354c4
9 changed files with 152 additions and 27 deletions

View File

@@ -4,6 +4,7 @@
#include "pch.h"
#include "MouseHighlighter.h"
#include "trace.h"
#include <cmath>
#ifdef COMPOSITION
namespace winrt
@@ -43,7 +44,7 @@ private:
void AddDrawingPoint(MouseButton button);
void UpdateDrawingPointPosition(MouseButton button);
void StartDrawingPointFading(MouseButton button);
void ClearDrawingPoint(MouseButton button);
void ClearDrawingPoint();
void ClearDrawing();
void BringToFront();
HHOOK m_mouseHook = NULL;
@@ -66,10 +67,12 @@ private:
winrt::CompositionSpriteShape m_leftPointer{ nullptr };
winrt::CompositionSpriteShape m_rightPointer{ nullptr };
winrt::CompositionSpriteShape m_alwaysPointer{ nullptr };
winrt::CompositionSpriteShape m_spotlightPointer{ nullptr };
bool m_leftPointerEnabled = true;
bool m_rightPointerEnabled = true;
bool m_alwaysPointerEnabled = true;
bool m_spotlightMode = false;
bool m_leftButtonPressed = false;
bool m_rightButtonPressed = false;
@@ -95,8 +98,7 @@ bool Highlighter::CreateHighlighter()
try
{
// We need a dispatcher queue.
DispatcherQueueOptions options =
{
DispatcherQueueOptions options = {
sizeof(options),
DQTYPE_THREAD_CURRENT,
DQTAT_COM_ASTA,
@@ -122,7 +124,8 @@ bool Highlighter::CreateHighlighter()
m_root.Children().InsertAtTop(m_shape);
return true;
} catch (...)
}
catch (...)
{
return false;
}
@@ -130,6 +133,9 @@ bool Highlighter::CreateHighlighter()
void Highlighter::AddDrawingPoint(MouseButton button)
{
if (!m_compositor)
return;
POINT pt;
// Applies DPIs.
@@ -141,6 +147,7 @@ void Highlighter::AddDrawingPoint(MouseButton button)
// Create circle and add it.
auto circleGeometry = m_compositor.CreateEllipseGeometry();
circleGeometry.Radius({ m_radius, m_radius });
auto circleShape = m_compositor.CreateSpriteShape(circleGeometry);
circleShape.Offset({ static_cast<float>(pt.x), static_cast<float>(pt.y) });
if (button == MouseButton::Left)
@@ -156,9 +163,22 @@ void Highlighter::AddDrawingPoint(MouseButton button)
else
{
// always
circleShape.FillBrush(m_compositor.CreateColorBrush(m_alwaysColor));
m_alwaysPointer = circleShape;
if (m_spotlightMode)
{
float borderThickness = static_cast<float>(std::hypot(GetSystemMetrics(SM_CXVIRTUALSCREEN), GetSystemMetrics(SM_CYVIRTUALSCREEN)));
circleGeometry.Radius({ static_cast<float>(borderThickness / 2.0 + m_radius), static_cast<float>(borderThickness / 2.0 + m_radius) });
circleShape.FillBrush(nullptr);
circleShape.StrokeBrush(m_compositor.CreateColorBrush(m_alwaysColor));
circleShape.StrokeThickness(borderThickness);
m_spotlightPointer = circleShape;
}
else
{
circleShape.FillBrush(m_compositor.CreateColorBrush(m_alwaysColor));
m_alwaysPointer = circleShape;
}
}
m_shape.Shapes().Append(circleShape);
// TODO: We're leaking shapes for long drawing sessions.
@@ -190,7 +210,20 @@ void Highlighter::UpdateDrawingPointPosition(MouseButton button)
else
{
// always
m_alwaysPointer.Offset({ static_cast<float>(pt.x), static_cast<float>(pt.y) });
if (m_spotlightMode)
{
if (m_spotlightPointer)
{
m_spotlightPointer.Offset({ static_cast<float>(pt.x), static_cast<float>(pt.y) });
}
}
else
{
if (m_alwaysPointer)
{
m_alwaysPointer.Offset({ static_cast<float>(pt.x), static_cast<float>(pt.y) });
}
}
}
}
void Highlighter::StartDrawingPointFading(MouseButton button)
@@ -229,20 +262,22 @@ void Highlighter::StartDrawingPointFading(MouseButton button)
circleShape.FillBrush().StartAnimation(L"Color", animation);
}
void Highlighter::ClearDrawingPoint(MouseButton _button)
void Highlighter::ClearDrawingPoint()
{
winrt::Windows::UI::Composition::CompositionSpriteShape circleShape{ nullptr };
if (nullptr == m_alwaysPointer)
if (m_spotlightMode)
{
// Guard against alwaysPointer not being initialized.
return;
if (m_spotlightPointer)
{
m_spotlightPointer.StrokeBrush().as<winrt::Windows::UI::Composition::CompositionColorBrush>().Color(winrt::Windows::UI::ColorHelper::FromArgb(0, 0, 0, 0));
}
}
else
{
if (m_alwaysPointer)
{
m_alwaysPointer.FillBrush().as<winrt::Windows::UI::Composition::CompositionColorBrush>().Color(winrt::Windows::UI::ColorHelper::FromArgb(0, 0, 0, 0));
}
}
// always
circleShape = m_alwaysPointer;
circleShape.FillBrush().as<winrt::Windows::UI::Composition::CompositionColorBrush>().Color(winrt::Windows::UI::ColorHelper::FromArgb(0, 0, 0, 0));
}
void Highlighter::ClearDrawing()
@@ -269,13 +304,14 @@ LRESULT CALLBACK Highlighter::MouseHookProc(int nCode, WPARAM wParam, LPARAM lPa
if (instance->m_alwaysPointerEnabled && !instance->m_rightButtonPressed)
{
// Clear AlwaysPointer only when it's enabled and RightPointer is not active
instance->ClearDrawingPoint(MouseButton::None);
instance->ClearDrawingPoint();
}
if (instance->m_leftButtonPressed)
{
// There might be a stray point from the user releasing the mouse button on an elevated window, which wasn't caught by us.
instance->StartDrawingPointFading(MouseButton::Left);
}
instance->AddDrawingPoint(MouseButton::Left);
instance->m_leftButtonPressed = true;
// start a timer for the scenario, when the user clicks a pinned window which has no focus.
@@ -293,7 +329,7 @@ LRESULT CALLBACK Highlighter::MouseHookProc(int nCode, WPARAM wParam, LPARAM lPa
if (instance->m_alwaysPointerEnabled && !instance->m_leftButtonPressed)
{
// Clear AlwaysPointer only when it's enabled and LeftPointer is not active
instance->ClearDrawingPoint(MouseButton::None);
instance->ClearDrawingPoint();
}
if (instance->m_rightButtonPressed)
{
@@ -358,13 +394,21 @@ void Highlighter::StartDrawing()
{
Logger::info("Starting draw mode.");
Trace::StartHighlightingSession();
if (m_spotlightMode && m_alwaysColor.A != 0)
{
Trace::StartSpotlightSession();
}
m_visible = true;
// 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);
ClearDrawing();
ShowWindow(m_hwnd, SW_SHOWNOACTIVATE);
instance->AddDrawingPoint(MouseButton::None);
instance->AddDrawingPoint(Highlighter::MouseButton::None);
m_mouseHook = SetWindowsHookEx(WH_MOUSE_LL, MouseHookProc, m_hinstance, 0);
}
@@ -377,6 +421,7 @@ void Highlighter::StopDrawing()
m_leftPointer = nullptr;
m_rightPointer = nullptr;
m_alwaysPointer = nullptr;
m_spotlightPointer = nullptr;
ShowWindow(m_hwnd, SW_HIDE);
UnhookWindowsHookEx(m_mouseHook);
ClearDrawing();
@@ -388,7 +433,8 @@ void Highlighter::SwitchActivationMode()
PostMessage(m_hwnd, WM_SWITCH_ACTIVATION_MODE, 0, 0);
}
void Highlighter::ApplySettings(MouseHighlighterSettings settings) {
void Highlighter::ApplySettings(MouseHighlighterSettings settings)
{
m_radius = static_cast<float>(settings.radius);
m_fadeDelay_ms = settings.fadeDelayMs;
m_fadeDuration_ms = settings.fadeDurationMs;
@@ -398,9 +444,23 @@ void Highlighter::ApplySettings(MouseHighlighterSettings settings) {
m_leftPointerEnabled = settings.leftButtonColor.A != 0;
m_rightPointerEnabled = settings.rightButtonColor.A != 0;
m_alwaysPointerEnabled = settings.alwaysColor.A != 0;
m_spotlightMode = settings.spotlightMode && settings.alwaysColor.A != 0;
if (m_spotlightMode)
{
m_leftPointerEnabled = false;
m_rightPointerEnabled = false;
}
if (instance->m_visible)
{
instance->StopDrawing();
instance->StartDrawing();
}
}
void Highlighter::BringToFront() {
void Highlighter::BringToFront()
{
// 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);
}
@@ -488,8 +548,7 @@ bool Highlighter::MyRegisterClass(HINSTANCE hInstance)
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;
return CreateWindowExW(exStyle, m_className, m_windowTitle, WS_POPUP, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, m_hwndOwner, nullptr, hInstance, nullptr) != nullptr;
}
void Highlighter::Terminate()

View File

@@ -18,6 +18,7 @@ struct MouseHighlighterSettings
int fadeDelayMs = MOUSE_HIGHLIGHTER_DEFAULT_DELAY_MS;
int fadeDurationMs = MOUSE_HIGHLIGHTER_DEFAULT_DURATION_MS;
bool autoActivate = MOUSE_HIGHLIGHTER_DEFAULT_AUTO_ACTIVATE;
bool spotlightMode = false;
};
int MouseHighlighterMain(HINSTANCE hinst, MouseHighlighterSettings settings);

View File

@@ -18,6 +18,7 @@ namespace
const wchar_t JSON_KEY_HIGHLIGHT_FADE_DELAY_MS[] = L"highlight_fade_delay_ms";
const wchar_t JSON_KEY_HIGHLIGHT_FADE_DURATION_MS[] = L"highlight_fade_duration_ms";
const wchar_t JSON_KEY_AUTO_ACTIVATE[] = L"auto_activate";
const wchar_t JSON_KEY_SPOTLIGHT_MODE[] = L"spotlight_mode";
}
extern "C" IMAGE_DOS_HEADER __ImageBase;
@@ -367,6 +368,16 @@ public:
{
Logger::warn("Failed to initialize auto activate from settings. Will use default value");
}
try
{
// Parse spotlight mode
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_SPOTLIGHT_MODE);
highlightSettings.spotlightMode = jsonPropertiesObject.GetNamedBoolean(JSON_KEY_VALUE);
}
catch (...)
{
Logger::warn("Failed to initialize spotlight mode settings. Will use default value");
}
}
else
{

View File

@@ -30,3 +30,13 @@ void Trace::StartHighlightingSession() noexcept
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE));
}
// Log that spotlight mode is enabled
void Trace::StartSpotlightSession() noexcept
{
TraceLoggingWriteWrapper(
g_hProvider,
"MouseHighlighter_StartSpotlightSession",
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE));
}

View File

@@ -10,4 +10,7 @@ public:
// Log that the user activated the module by starting a highlighting session
static void StartHighlightingSession() noexcept;
// Log that spotlight mode is enabled
static void StartSpotlightSession() noexcept;
};