Ready for Review - [Mouse Jump] - port upstream WinUI3 code to Mouse Jump (microsoft#48290) (#48393)

<!-- 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

Changes for #48290 to convert Mouse Jump to a WinUI app and remove all
remaining dependencies on WinForms, based on work already done in the
original "FancyMouse" project
(https://github.com/mikeclayton/FancyMouse).

## Notes for reviewers

* the new WinUI build of the app is output into the "/WinUI3Apps"
subfolder

* there's 2 new assemblies that need to be added to the installation as
well - MouseJump.HotKeys.dll and MouseJump.Models.dll. I'm not sure how
to add those to the installer for signing / shipping...

---

### Summary of changes

* New thumbnail layout and rendering code
* WinUI rewrite (winforms version still committed)
* MouseJump.Kicker (dev launch tool)
* CsWin32 for interop
* New assemblies - code reorganised

---

### 1. New layout code

Incorporates latest FancyMouse core layout and rendering logic into
Mouse Jump:

* includes **support** for multiple devices in layout algorithms
* preview still only shows local machine though
* prerequisite for long-term goal #34126

<img width="650" height="709" alt="image"
src="https://github.com/user-attachments/assets/9d1d996d-ed05-4471-b8a5-bd93442f70dc"
/>

### 2. WinUI rewrite

Port latest stable FancyMouse WinUI implementation into Mouse Jump.

Existing WinForms UI left in-situ side-by-side for now - easy to delete
if not needed.

### 3. MouseJump.Kicker

A small dev utility to start Mouse Jump without needing to build the
runner project:

<img width="283" height="274" alt="image"
src="https://github.com/user-attachments/assets/c81bb3f5-5008-48ce-8bc0-eef18413dee6"
/>

### 4. Cswin32 for interop

All win32 interop is now accessed via CsWin32 bindings.

The original win32 bindings were heavily influenced by CsWin32 generated
code (e.g. ```BOOL```, ```HWND```, etc structs), so there's not actually
much change other than deleting a lot of boilerplate code.

### 5. New assemblies

Some code has been reorganised into new assemblies to make it easier to
keep Mouse Jump in sync with upstream FancyMouse

* MouseJump.HotKeys
* MouseJump.Models




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

- [x] Closes #48290
- [x] **Communication:** I've discussed this with core contributors
already. If 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
  -  all ui strings come from Resources.resw
- [x] **Dev docs:** Added/updated
  -  no changes required
- [x] **New binaries:** Added on the required places
  -  no changes required
- [x] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [x] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [x] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [x] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [x] **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
  -  no changes required
<!-- 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

- **Workflow tests**
  - [x] Automated tests passing locally
  - [x] Minimal actions workflow (spelling check) passing for PR
  - [ ] Full actions workflow (msbuild) passing for PR
- **UI tests**
  - [x] Happy path
    - [x] preview image appears when activated
- [x] clicking the preview image moves the mouse cursor to the correct
location
- [x] right-click dismisses the preview image without moving the mouse
- [x] pressing escape dismisses the preview image without moving the
mouse
- [x] left or right clicking another application / desktop dismisses the
preview image without moving the mouse
- [x] Works on multiple monitors with different dpi scaling settings
(e.g. 100% vs 150%)
*
https://github.com/microsoft/PowerToys/pull/23566#issuecomment-1411869418
*
https://github.com/microsoft/PowerToys/pull/23566#issuecomment-1412834413
- [x] Handling negative coordinates on non-primary monitors if higher or
"lefter" than primary monitor
*
https://github.com/microsoft/PowerToys/pull/23566#issuecomment-1404931694
- [x] Mouse crosshair moves when Mouse Jump moves the cursor (mouse
clicks *and* keyboard shortcuts)
    * #24523
    * #24527
- [x] Activating when the preview window is already visible moves the
form to the new mouse position
- [x] Number and key shortcuts (1-9, Home / End, Left / Right Arrow)
jump to the appropriate monitor
  - [x] Number-pad shortcuts (1-9 jump to the appropriate monitor
- **Settings tests**
- [x] Changing thumbnail size settings updates the size of the thumbnail
- [x] Changing preview type between Compact, Bezelled and Custom shows
the correct preview type
  - [x] Changing custom preview settings shows the correct settings
- [ ] Launching with settings version 1.0 upgrades settings to version
1.1, with "Bezelled" as the default style and the "Custom" settings
preconfigured to match "Bezelled"
- **Lifecycle tests**
- [x] Starting PowerToys Runner launches MouseJump exe when enabled, and
not when disabled
- [x] Enabling / disabling Mouse Jump in settings starts / stops
MouseJump exe
  - [x] Exiting PowerToys Runner stops MouseJump exe
  - [x] Killing runner exe via Task Manager stops MouseJump exe
  - [x] Stopping Visual Studio local debug run stops MouseJump exe
- note - runner needs to be in *non*-admin mode otherwise Visual Studio
debugger disconnects at launch
- [x] Hotkey and size settings are automatically reloaded when config
file is modified from Settings UI
- [ ] ~~Hotkey and size settings are automatically reloaded when config
file is modified manually (e.g. in notepad) while runner and
MouseJumpUI.exe are running~~
- **[Internal Test
Suite](5bc7201ae2/doc/releases/tests-checklist-template.md (mouse-utils))**
  - [x] Enable Mouse Jump. Then:
- [x] Press the activation shortcut and verify the screens preview
appears.
- [x] Change activation shortcut and verify that new shortcut triggers
Mouse Jump.
- [x] Click around the screen preview and ensure that mouse cursor
jumped to clicked location.
- [x] Reorder screens in Display settings and confirm that Mouse Jump
reflects the change and still works correctly.
- [x] Change scaling of screens and confirm that Mouse Jump still works
correctly.
- [ ] Unplug additional monitors and confirm that Mouse Jump still works
correctly.
- [x] Disable Mouse Jump and verify that the module is not activated
when you press the activation shortcut.

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Boliang Zhang (from Dev Box) <bozhang@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: moooyo <42196638+moooyo@users.noreply.github.com>
Co-authored-by: Yu Leng (from Dev Box) <yuleng@microsoft.com>
Copilot-Session: 8e04a72e-3b0f-4ac4-8156-d04ea9b8bb85
This commit is contained in:
Michael Clayton
2026-08-07 06:34:15 +01:00
committed by GitHub
parent 9fcb8faac5
commit e0010c5642
154 changed files with 6325 additions and 3464 deletions

View File

@@ -114,7 +114,7 @@
^src/modules/cmdpal/Tests/Microsoft\.CmdPal\.Common\.UnitTests/Text/.*\.cs$ ^src/modules/cmdpal/Tests/Microsoft\.CmdPal\.Common\.UnitTests/Text/.*\.cs$
^src/modules/colorPicker/ColorPickerUI/Shaders/GridShader\.cso$ ^src/modules/colorPicker/ColorPickerUI/Shaders/GridShader\.cso$
^src/modules/launcher/Plugins/Microsoft\.PowerToys\.Run\.Plugin\.TimeDate/Properties/ ^src/modules/launcher/Plugins/Microsoft\.PowerToys\.Run\.Plugin\.TimeDate/Properties/
^src/modules/MouseUtils/MouseJumpUI/MainForm\.resx$ ^src/modules/MouseUtils/MouseJumpUI/UI/MainForm\.resx$
^src/modules/MouseWithoutBorders/App/.*/NativeMethods\.cs$ ^src/modules/MouseWithoutBorders/App/.*/NativeMethods\.cs$
^src/modules/MouseWithoutBorders/App/Form/.*\.Designer\.cs$ ^src/modules/MouseWithoutBorders/App/Form/.*\.Designer\.cs$
^src/modules/MouseWithoutBorders/App/Form/.*\.resx$ ^src/modules/MouseWithoutBorders/App/Form/.*\.resx$
@@ -139,6 +139,7 @@
^src/modules/ZoomIt/ZoomIt/selfie_segmentation\.onnx$ ^src/modules/ZoomIt/ZoomIt/selfie_segmentation\.onnx$
^src/modules/ZoomIt/ZoomIt/ZoomIt\.idc$ ^src/modules/ZoomIt/ZoomIt/ZoomIt\.idc$
^src/Monaco/ ^src/Monaco/
^tools/MouseJump.Kicker/KickerForm\.resx$
^tools/project_template/ModuleTemplate/resource\.h$ ^tools/project_template/ModuleTemplate/resource\.h$
^tools/Verification scripts/Check preview handler registration\.ps1$ ^tools/Verification scripts/Check preview handler registration\.ps1$
ignore$ ignore$

View File

@@ -47,7 +47,6 @@ ALLVIEW
ALPHATYPE ALPHATYPE
altkey altkey
amr amr
ANDSCANS
animatedvisuals animatedvisuals
Animnate Animnate
AOC AOC
@@ -194,6 +193,7 @@ CAPTURECHANGED
CARETBLINKING CARETBLINKING
carlos carlos
Carlseibert Carlseibert
cascadia
caseinsensitive caseinsensitive
caub caub
CBN CBN
@@ -313,7 +313,6 @@ CREATESCHEDULEDTASK
CREATESTRUCT CREATESTRUCT
CREATEWINDOWFAILED CREATEWINDOWFAILED
creativecommons creativecommons
CRECT
CRH CRH
critsec critsec
cropandlock cropandlock
@@ -389,7 +388,6 @@ DELA
DELD DELD
deld deld
DELETEDKEYIMAGE DELETEDKEYIMAGE
DELETESCANS
DEMOTYPE DEMOTYPE
DENORMAL DENORMAL
depersist depersist
@@ -457,10 +455,8 @@ dreamsofameaningfullife
drivedetectionwarning drivedetectionwarning
DROPFILES DROPFILES
DSPDLOG DSPDLOG
DSTINVERT
DString DString
dto dto
DUMMYUNIONNAME
dumpbin dumpbin
dutil dutil
DVASPECT DVASPECT
@@ -505,6 +501,8 @@ encodedlaunch
encryptor encryptor
ENDSESSION ENDSESSION
ENSUREVISIBLE ENSUREVISIBLE
ENTERIDLE
ENTERMENULOOP
ENTERSIZEMOVE ENTERSIZEMOVE
ENTRYW ENTRYW
ENU ENU
@@ -538,6 +536,8 @@ EXECUTEDEFAULT
executionpolicy executionpolicy
exename exename
exf exf
EXITCOMMAND
EXITMENULOOP
EXITSIZEMOVE EXITSIZEMOVE
exlist exlist
EXPCMDFLAGS EXPCMDFLAGS
@@ -669,6 +669,7 @@ gwl
GWLP GWLP
GWLSTYLE GWLSTYLE
hangeul hangeul
Hanguel
Hantai Hantai
Hanzi Hanzi
Hardlines Hardlines
@@ -812,6 +813,7 @@ imageresizerinput
imageresizersettings imageresizersettings
imagingdevices imagingdevices
ime ime
IMEAceept
imgflip imgflip
inapp inapp
inbox inbox
@@ -826,6 +828,8 @@ INITCOMMONCONTROLSEX
INITDIALOG INITDIALOG
INITGUID INITGUID
initialfile initialfile
INITMENU
INITMENUPOPUP
INITTOLOGFONTSTRUCT INITTOLOGFONTSTRUCT
inkscape inkscape
INLINEPREFIX INLINEPREFIX
@@ -930,6 +934,7 @@ Lclean
Ldone Ldone
Ldr Ldr
LEFTALIGN LEFTALIGN
LEFTBUTTON
leftclick leftclick
LEFTSCROLLBAR LEFTSCROLLBAR
LEFTTEXT LEFTTEXT
@@ -981,11 +986,9 @@ LPCLSID
lpcmi lpcmi
LPCMINVOKECOMMANDINFO LPCMINVOKECOMMANDINFO
LPCREATESTRUCT LPCREATESTRUCT
LPCRECT
LPCTSTR LPCTSTR
lpdw lpdw
lpfn lpfn
LPINPUT
LPITEMIDLIST LPITEMIDLIST
lpmi lpmi
LPMINMAXINFO LPMINMAXINFO
@@ -994,6 +997,7 @@ LPOSVERSIONINFOEXW
LPQUERY LPQUERY
lprc lprc
LPSAFEARRAY LPSAFEARRAY
LPSECURITY
lpstr lpstr
lpsz lpsz
LPTHREAD LPTHREAD
@@ -1054,8 +1058,7 @@ meme
memicmp memicmp
MENUITEMINFO MENUITEMINFO
MENUITEMINFOW MENUITEMINFOW
MERGECOPY MENUSELECT
MERGEPAINT
Metacharacter Metacharacter
metadatamatters metadatamatters
Metadatas Metadatas
@@ -1202,7 +1205,6 @@ NOASYNC
NOCHANGEDIR NOCHANGEDIR
NOCLIP NOCLIP
NOCLOSEPROCESS NOCLOSEPROCESS
NOCOALESCE
NOCOMM NOCOMM
NOCONFIRMMKDIR NOCONFIRMMKDIR
NOCOPYBITS NOCOPYBITS
@@ -1219,10 +1221,10 @@ NOLINKINFO
nologo nologo
NOMCX NOMCX
NOMINMAX NOMINMAX
NOMIRRORBITMAP
nomove nomove
NONANTIALIASED NONANTIALIASED
nonclient nonclient
Nonconvert
NONCLIENTMETRICSW NONCLIENTMETRICSW
NONELEVATED NONELEVATED
nonspace nonspace
@@ -1251,8 +1253,6 @@ NOTIFYICONDATAW
NOTIMPL NOTIMPL
NOTOPMOST NOTOPMOST
NOTRACK NOTRACK
NOTSRCCOPY
NOTSRCERASE
Notupdated Notupdated
notwindows notwindows
NOTXORPEN NOTXORPEN
@@ -1277,7 +1277,10 @@ nwc
ocr ocr
Ocrsettings Ocrsettings
odbccp odbccp
Oemcomma
OEMCONVERT OEMCONVERT
Oemplus
Oemtilde
officehubintl officehubintl
OFN OFN
ofs ofs
@@ -1302,7 +1305,6 @@ openxmlformats
OPTIMIZEFORINVOKE OPTIMIZEFORINVOKE
Optronics Optronics
ORPHANEDDIALOGTITLE ORPHANEDDIALOGTITLE
ORSCANS
oss oss
OSVERSIONINFO OSVERSIONINFO
OSVERSIONINFOEX OSVERSIONINFOEX
@@ -1334,10 +1336,7 @@ PARENTRELATIVEPARSING
parray parray
parseable parseable
PARTIALCONFIRMATIONDIALOGTITLE PARTIALCONFIRMATIONDIALOGTITLE
PATCOPY
PATHMUSTEXIST PATHMUSTEXIST
PATINVERT
PATPAINT
pbc pbc
pbi pbi
PBP PBP
@@ -1567,6 +1566,7 @@ REMAPSUCCESSFUL
REMAPUNSUCCESSFUL REMAPUNSUCCESSFUL
Remotable Remotable
remotedesktop remotedesktop
remotehost
remoteip remoteip
Removelnk Removelnk
renamable renamable
@@ -1603,7 +1603,6 @@ RIGHTBUTTON
RIGHTSCROLLBAR RIGHTSCROLLBAR
riid riid
rollups rollups
rop
ROUNDSMALL ROUNDSMALL
rpcrt rpcrt
RRF RRF
@@ -1717,6 +1716,7 @@ shortdate
SHORTPATH SHORTPATH
shortsplit shortsplit
showcolorname showcolorname
SHOWCONTEXTMENU
SHOWDEFAULT SHOWDEFAULT
SHOWELEVATIONPROMPT SHOWELEVATIONPROMPT
SHOWMAGNIFIEDCURSOR SHOWMAGNIFIEDCURSOR
@@ -1779,12 +1779,8 @@ spsi
spsia spsia
spsrm spsrm
spsv spsv
SRCAND
SRCCOPY SRCCOPY
SRCERASE
Srch Srch
SRCINVERT
SRCPAINT
SResize SResize
srme srme
srre srre
@@ -1948,6 +1944,7 @@ tracerpt
trackbar trackbar
trafficmanager trafficmanager
transicc transicc
TRAYICON
TRANSIENTWINDOW TRANSIENTWINDOW
transitioning transitioning
TRAYMOUSEMESSAGE TRAYMOUSEMESSAGE
@@ -1966,6 +1963,8 @@ TYPEMOUSE
TYPESHORTCUT TYPESHORTCUT
UAC UAC
UACUI UACUI
UAHINITMENU
UAHMEASUREMENUITEM
UAL UAL
uap uap
UBR UBR
@@ -1990,6 +1989,7 @@ unescaped
unforgeable unforgeable
ungroup ungroup
UNICODETEXT UNICODETEXT
UNINITMENUPOPUP
unins unins
uninsdeletekey uninsdeletekey
Uninstaller Uninstaller
@@ -2060,7 +2060,6 @@ vgetq
viewmodels viewmodels
virama virama
VIRTKEY VIRTKEY
VIRTUALDESK
VISEGRADRELAY VISEGRADRELAY
visiblecolorformats visiblecolorformats
visio visio

View File

@@ -545,11 +545,19 @@
<Platform Solution="*|ARM64" Project="ARM64" /> <Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" /> <Platform Solution="*|x64" Project="x64" />
</Project> </Project>
<Project Path="src/modules/MouseUtils/MouseJump/MouseJump.vcxproj" Id="8a08d663-4995-40e3-b42c-3f910625f284" /> <Project Path="src/modules/MouseUtils/MouseJump.HotKeys/MouseJump.HotKeys.csproj">
<Project Path="src/modules/MouseUtils/MouseJumpUI/MouseJumpUI.csproj">
<Platform Solution="*|ARM64" Project="ARM64" /> <Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" /> <Platform Solution="*|x64" Project="x64" />
</Project> </Project>
<Project Path="src/modules/MouseUtils/MouseJump.Models/MouseJump.Models.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/MouseUtils/MouseJump.WinUI3/MouseJump.WinUI3.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/MouseUtils/MouseJump/MouseJump.vcxproj" Id="8a08d663-4995-40e3-b42c-3f910625f284" />
<Project Path="src/modules/MouseUtils/MousePointerCrosshairs/MousePointerCrosshairs.vcxproj" Id="eae14c0e-7a6b-45da-9080-a7d8c077ba6e" /> <Project Path="src/modules/MouseUtils/MousePointerCrosshairs/MousePointerCrosshairs.vcxproj" Id="eae14c0e-7a6b-45da-9080-a7d8c077ba6e" />
</Folder> </Folder>
<Folder Name="/modules/MouseUtils/Tests/"> <Folder Name="/modules/MouseUtils/Tests/">
@@ -557,6 +565,14 @@
<Platform Solution="*|ARM64" Project="ARM64" /> <Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" /> <Platform Solution="*|x64" Project="x64" />
</Project> </Project>
<Project Path="src/modules/MouseUtils/MouseJump.HotKeys.UnitTests/MouseJump.HotKeys.UnitTests.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/MouseUtils/MouseJump.Models.UnitTests/MouseJump.Models.UnitTests.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/MouseUtils/MouseUtils.UITests/MouseUtils.UITests.csproj"> <Project Path="src/modules/MouseUtils/MouseUtils.UITests/MouseUtils.UITests.csproj">
<Platform Solution="*|ARM64" Project="ARM64" /> <Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" /> <Platform Solution="*|x64" Project="x64" />

View File

@@ -2,29 +2,32 @@
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System.Globalization; using System.Drawing;
using System.Drawing.Imaging;
using System.Reflection; using System.Reflection;
using System.Text;
using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.VisualStudio.TestTools.UnitTesting;
using MouseJump.Common.Helpers; using MouseJump.Common.Helpers;
using MouseJump.Common.Imaging; using MouseJump.Common.Imaging;
using MouseJump.Common.Models.Drawing; using MouseJump.Models.Display;
using MouseJump.Common.Models.Styles; using MouseJump.Models.Drawing;
using MouseJump.Models.Styles;
namespace MouseJump.Common.UnitTests.Helpers; namespace MouseJump.Common.UnitTests.Helpers;
[TestClass]
public static class DrawingHelperTests public static class DrawingHelperTests
{ {
[TestClass] [TestClass]
public sealed class RenderPreviewTests public sealed class GetPreviewLayoutTests
{ {
public sealed class TestCase public sealed class TestCase
{ {
public TestCase(PreviewStyle previewStyle, List<RectangleInfo> screens, PointInfo activatedLocation, string desktopImageFilename, string expectedImageFilename) public TestCase(PreviewStyle previewStyle, DisplayInfo displayInfo, ScreenInfo activatedScreen, PointInfo activatedLocation, string desktopImageFilename, string expectedImageFilename)
{ {
this.PreviewStyle = previewStyle; this.PreviewStyle = previewStyle;
this.Screens = screens; this.DisplayInfo = displayInfo;
this.ActivatedScreen = activatedScreen;
this.ActivatedLocation = activatedLocation; this.ActivatedLocation = activatedLocation;
this.DesktopImageFilename = desktopImageFilename; this.DesktopImageFilename = desktopImageFilename;
this.ExpectedImageFilename = expectedImageFilename; this.ExpectedImageFilename = expectedImageFilename;
@@ -32,7 +35,9 @@ public static class DrawingHelperTests
public PreviewStyle PreviewStyle { get; } public PreviewStyle PreviewStyle { get; }
public List<RectangleInfo> Screens { get; } public DisplayInfo DisplayInfo { get; }
public ScreenInfo ActivatedScreen { get; }
public PointInfo ActivatedLocation { get; } public PointInfo ActivatedLocation { get; }
@@ -43,32 +48,94 @@ public static class DrawingHelperTests
public static IEnumerable<object[]> GetTestCases() public static IEnumerable<object[]> GetTestCases()
{ {
/* 4-grid */ // colors like "SystemColors.Highlight" can change between versions of windows,
// so we'll use hard-coded colors instead to reduce false test failures
var systemHighlight = Color.FromArgb(0, 120, 215);
var previewStyle = StyleHelper.BezelledPreviewStyle;
previewStyle = new(
canvasSize: previewStyle.CanvasSize,
canvasStyle: new(
marginStyle: previewStyle.CanvasStyle.MarginStyle,
borderStyle: previewStyle.CanvasStyle.BorderStyle.WithColor(systemHighlight),
paddingStyle: previewStyle.CanvasStyle.PaddingStyle,
backgroundStyle: previewStyle.CanvasStyle.BackgroundStyle
),
screenStyle: previewStyle.ScreenStyle,
extraColors: previewStyle.ExtraColors
);
// 4-grid
var displayInfo = new DisplayInfo(
devices: new DeviceInfo[]
{
new(
hostname: "localhost",
localhost: true,
screens: new List<ScreenInfo>
{
new(
handle: 0,
primary: true,
displayArea: new(0, 0, 500, 500),
workingArea: new(0, 0, 500, 500)),
new(
handle: 0,
primary: false,
displayArea: new(500, 0, 500, 500),
workingArea: new(500, 0, 500, 500)),
new(
handle: 0,
primary: false,
displayArea: new(500, 500, 500, 500),
workingArea: new(500, 500, 500, 500)),
new(
handle: 0,
primary: false,
displayArea: new(0, 500, 500, 500),
workingArea: new(0, 500, 500, 500)),
}
),
});
yield return new object[] yield return new object[]
{ {
new TestCase( new TestCase(
previewStyle: StyleHelper.BezelledPreviewStyle, previewStyle: previewStyle,
screens: new List<RectangleInfo>() displayInfo: displayInfo,
{ activatedScreen: displayInfo.Devices[0].Screens[0],
new(0, 0, 500, 500),
new(500, 0, 500, 500),
new(500, 500, 500, 500),
new(0, 500, 500, 500),
},
activatedLocation: new(x: 50, y: 50), activatedLocation: new(x: 50, y: 50),
desktopImageFilename: "_test-4grid-desktop.png", desktopImageFilename: "_test-4grid-desktop.png",
expectedImageFilename: "_test-4grid-expected.png"), expectedImageFilename: "_test-4grid-expected.png"),
}; };
/* win 11 */
// win 11
displayInfo = new(
devices: new DeviceInfo[]
{
new(
hostname: "localhost",
localhost: true,
screens: new List<ScreenInfo>
{
new(
handle: 0,
primary: true,
displayArea: new(5120, 349, 1920, 1080),
workingArea: new(5120, 349, 1920, 1080)),
new(
handle: 0,
primary: false,
displayArea: new(0, 0, 5120, 1440),
workingArea: new(0, 0, 5120, 1440)),
}
),
}
);
yield return new object[] yield return new object[]
{ {
new TestCase( new TestCase(
previewStyle: StyleHelper.BezelledPreviewStyle, previewStyle: previewStyle,
screens: new List<RectangleInfo>() displayInfo: displayInfo,
{ activatedScreen: displayInfo.Devices[0].Screens[0],
new(5120, 349, 1920, 1080),
new(0, 0, 5120, 1440),
},
activatedLocation: new(x: 50, y: 50), activatedLocation: new(x: 50, y: 50),
desktopImageFilename: "_test-win11-desktop.png", desktopImageFilename: "_test-win11-desktop.png",
expectedImageFilename: "_test-win11-expected.png"), expectedImageFilename: "_test-win11-expected.png"),
@@ -76,22 +143,36 @@ public static class DrawingHelperTests
} }
[TestMethod] [TestMethod]
[DynamicData(nameof(GetTestCases), DynamicDataSourceType.Method)] [DynamicData(nameof(GetTestCases))]
public void RunTestCases(TestCase data) public async Task RunTestCases(TestCase data)
{ {
// load the fake desktop image // load the fake desktop image
using var desktopImage = RenderPreviewTests.LoadImageResource(data.DesktopImageFilename); using var desktopImage = GetPreviewLayoutTests.LoadImageResource(data.DesktopImageFilename);
var formLayout = LayoutHelper.GetFormLayout(
previewStyle: data.PreviewStyle,
displayInfo: data.DisplayInfo,
activatedScreen: data.ActivatedScreen,
activatedLocation: data.ActivatedLocation);
var imageCopyServices = formLayout.CanvasLayout.DeviceLayouts
.Select(
deviceLayout => (IImageRegionCopyService)new StaticImageRegionCopyService(desktopImage))
.ToList();
// draw the preview image // draw the preview image
var previewLayout = LayoutHelper.GetPreviewLayout( using var actual = await DrawingHelper.RenderPreviewAsync(formLayout.CanvasLayout, data.ActivatedScreen, imageCopyServices);
previewStyle: data.PreviewStyle,
screens: data.Screens, // save the actual image so we can pick it up as a build artifact
activatedLocation: data.ActivatedLocation); var actualFilename = Path.GetFileNameWithoutExtension(data.ExpectedImageFilename) + "_actual" + Path.GetExtension(data.ExpectedImageFilename);
var imageCopyService = new StaticImageRegionCopyService(desktopImage); actual.Save(actualFilename, ImageFormat.Png);
using var actual = DrawingHelper.RenderPreview(previewLayout, imageCopyService);
// load the expected image // load the expected image
var expected = RenderPreviewTests.LoadImageResource(data.ExpectedImageFilename); var expected = GetPreviewLayoutTests.LoadImageResource(data.ExpectedImageFilename);
// save the actual image so we can pick it up as a build artifact
var expectedFilename = Path.GetFileNameWithoutExtension(data.ExpectedImageFilename) + "_expected" + Path.GetExtension(data.ExpectedImageFilename);
expected.Save(expectedFilename, ImageFormat.Png);
// compare the images // compare the images
AssertImagesEqual(expected, actual); AssertImagesEqual(expected, actual);
@@ -99,24 +180,26 @@ public static class DrawingHelperTests
private static Bitmap LoadImageResource(string filename) private static Bitmap LoadImageResource(string filename)
{ {
var assembly = Assembly.GetExecutingAssembly(); var referenceType = typeof(DrawingHelperTests);
var assemblyName = new AssemblyName(assembly.FullName ?? throw new InvalidOperationException()); var resourceAssembly = referenceType.Assembly;
var resourceName = $"{typeof(DrawingHelperTests).Namespace}.{filename.Replace("/", ".")}";
var resourceNames = assembly.GetManifestResourceNames(); var resourcePrefix = referenceType.Namespace;
var resourceName = $"{resourcePrefix}.{filename.Replace("/", ".")}";
var resourceNames = resourceAssembly.GetManifestResourceNames();
if (!resourceNames.Contains(resourceName)) if (!resourceNames.Contains(resourceName))
{ {
var message = new StringBuilder(); var message = string.Join(
message.AppendLine(CultureInfo.InvariantCulture, $"Embedded resource '{resourceName}' does not exist."); Environment.NewLine,
message.AppendLine($"Known resources:"); new List<string>([
foreach (var name in resourceNames) $"Embedded resource '{resourceName}' does not exist.",
{ string.Empty,
message.AppendLine(name); "Valid resource names are:",
} string.Empty])
.Concat(resourceNames));
throw new InvalidOperationException(message.ToString()); throw new InvalidOperationException(message);
} }
var stream = assembly.GetManifestResourceStream(resourceName) var stream = resourceAssembly.GetManifestResourceStream(resourceName)
?? throw new InvalidOperationException(); ?? throw new InvalidOperationException();
var image = (Bitmap)Image.FromStream(stream); var image = (Bitmap)Image.FromStream(stream);
return image; return image;
@@ -143,12 +226,11 @@ public static class DrawingHelperTests
var actualPixel = actual.GetPixel(x, y); var actualPixel = actual.GetPixel(x, y);
// allow a small tolerance for rounding differences in gdi // allow a small tolerance for rounding differences in gdi
// using a tolerance of 3 for support of minor differences in Windows Server 2025 CI
Assert.IsTrue( Assert.IsTrue(
(Math.Abs(expectedPixel.A - actualPixel.A) <= 3) && (Math.Abs(expectedPixel.A - actualPixel.A) <= 1) &&
(Math.Abs(expectedPixel.R - actualPixel.R) <= 3) && (Math.Abs(expectedPixel.R - actualPixel.R) <= 1) &&
(Math.Abs(expectedPixel.G - actualPixel.G) <= 3) && (Math.Abs(expectedPixel.G - actualPixel.G) <= 1) &&
(Math.Abs(expectedPixel.B - actualPixel.B) <= 3), (Math.Abs(expectedPixel.B - actualPixel.B) <= 1),
$"images differ at pixel ({x}, {y}) - expected: {expectedPixel}, actual: {actualPixel}"); $"images differ at pixel ({x}, {y}) - expected: {expectedPixel}, actual: {actualPixel}");
} }
} }

View File

@@ -2,17 +2,20 @@
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System.Diagnostics;
using System.Drawing;
using System.Text.Json; using System.Text.Json;
using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.VisualStudio.TestTools.UnitTesting;
using MouseJump.Common.Helpers; using MouseJump.Common.Helpers;
using MouseJump.Common.Models.Drawing; using MouseJump.Models.Display;
using MouseJump.Common.Models.Layout; using MouseJump.Models.Drawing;
using MouseJump.Common.Models.Styles; using MouseJump.Models.Styles;
using MouseJump.Models.ViewModel;
namespace MouseJump.Common.UnitTests.Helpers; namespace MouseJump.Common.UnitTests.Helpers;
[TestClass]
public static class LayoutHelperTests public static class LayoutHelperTests
{ {
/* /*
@@ -109,26 +112,32 @@ public static class LayoutHelperTests
{ {
public sealed class TestCase public sealed class TestCase
{ {
public TestCase(PreviewStyle previewStyle, List<RectangleInfo> screens, PointInfo activatedLocation, PreviewLayout expectedResult) public TestCase(string testName, PreviewStyle previewStyle, DisplayInfo displayInfo, ScreenInfo activatedScreen, PointInfo activatedLocation, FormViewModel expectedResult)
{ {
this.TestName = testName;
this.PreviewStyle = previewStyle; this.PreviewStyle = previewStyle;
this.Screens = screens; this.DisplayInfo = displayInfo;
this.ActivatedLocation = activatedLocation; this.ActivatedLocation = activatedLocation;
this.ActivatedScreen = activatedScreen;
this.ExpectedResult = expectedResult; this.ExpectedResult = expectedResult;
} }
public string TestName { get; }
public PreviewStyle PreviewStyle { get; } public PreviewStyle PreviewStyle { get; }
public List<RectangleInfo> Screens { get; } public DisplayInfo DisplayInfo { get; }
public ScreenInfo ActivatedScreen { get; }
public PointInfo ActivatedLocation { get; } public PointInfo ActivatedLocation { get; }
public PreviewLayout ExpectedResult { get; } public FormViewModel ExpectedResult { get; }
} }
public static IEnumerable<object[]> GetTestCases() public static IEnumerable<object[]> GetTestCases()
{ {
// happy path - single screen with 50% scaling, // happy path - single device with screen and 50% scaling,
// *has* a preview border but *no* screenshot borders // *has* a preview border but *no* screenshot borders
// //
// +----------------+ // +----------------+
@@ -136,10 +145,11 @@ public static class LayoutHelperTests
// | 0 | // | 0 |
// | | // | |
// +----------------+ // +----------------+
var testName = "Test 1 - Happy Path";
var previewStyle = new PreviewStyle( var previewStyle = new PreviewStyle(
canvasSize: new( canvasSize: new(
width: 524, width: (1024 / 2) + (5 * 2) + (1 * 2), // half the screen size, plus additional room for canvas border and padding
height: 396 height: (768 / 2) + (5 * 2) + (1 * 2) // half the screen size, plus additional room for canvas border and padding
), ),
canvasStyle: new( canvasStyle: new(
marginStyle: MarginStyle.Empty, marginStyle: MarginStyle.Empty,
@@ -154,38 +164,56 @@ public static class LayoutHelperTests
color2: Color.FromArgb(3, 68, 192) // darker blue color2: Color.FromArgb(3, 68, 192) // darker blue
) )
), ),
screenStyle: BoxStyle.Empty); screenStyle: BoxStyle.Empty,
var screens = new List<RectangleInfo> extraColors: Array.Empty<Color>());
{ var displayInfo = new DisplayInfo(
new(0, 0, 1024, 768), devices: new List<DeviceInfo>
};
var activatedLocation = new PointInfo(512, 384);
var expectedResult = new PreviewLayout(
virtualScreen: new(0, 0, 1024, 768),
screens: screens,
activatedScreenIndex: 0,
formBounds: new(250, 186, 524, 396),
previewStyle: previewStyle,
previewBounds: new(
outerBounds: new(0, 0, 524, 396),
marginBounds: new(0, 0, 524, 396),
borderBounds: new(0, 0, 524, 396),
paddingBounds: new(5, 5, 514, 386),
contentBounds: new(6, 6, 512, 384)
),
screenshotBounds: new()
{ {
new( new(
outerBounds: new(6, 6, 512, 384), hostname: "localhost",
marginBounds: new(6, 6, 512, 384), localhost: true,
borderBounds: new(6, 6, 512, 384), screens: new List<ScreenInfo>
paddingBounds: new(6, 6, 512, 384), {
contentBounds: new(6, 6, 512, 384) new(
handle: 0,
primary: true,
displayArea: new(0, 0, 1024, 768),
workingArea: new(0, 0, 1024, 768)),
}
), ),
}); });
yield return new object[] { new TestCase(previewStyle, screens, activatedLocation, expectedResult) }; var activatedScreen = displayInfo.Devices[0].Screens[0];
var activatedLocation = activatedScreen.DisplayArea.Midpoint;
var expectedResult = new FormViewModel(
formBounds: new(250, 186, 524, 396),
canvasLayout: new(
canvasBounds: BoxBounds.CreateFromOuterBounds(
outerBounds: new(0, 0, 524, 396),
boxStyle: previewStyle.CanvasStyle),
canvasStyle: previewStyle.CanvasStyle,
deviceLayouts: new List<DeviceViewModel>()
{
new(
deviceInfo: displayInfo.Devices[0],
deviceBounds: BoxBounds.CreateFromOuterBounds(
outerBounds: new(6, 6, 512, 384),
boxStyle: BoxStyle.Empty),
deviceStyle: BoxStyle.Empty,
screenLayouts: new List<ScreenViewModel>
{
new(
screenInfo: displayInfo.Devices[0].Screens[0],
screenBounds: BoxBounds.CreateFromOuterBounds(
outerBounds: new(6, 6, 512, 384),
boxStyle: previewStyle.ScreenStyle),
screenStyle: previewStyle.ScreenStyle),
}
),
}
));
yield return new object[] { new TestCase(testName, previewStyle, displayInfo, activatedScreen, activatedLocation, expectedResult) };
// happy path - single screen with 50% scaling, // happy path - single device with screen and 50% scaling,
// *no* preview borders but *has* screenshot borders // *no* preview borders but *has* screenshot borders
// //
// +----------------+ // +----------------+
@@ -193,10 +221,11 @@ public static class LayoutHelperTests
// | 0 | // | 0 |
// | | // | |
// +----------------+ // +----------------+
testName = "Test 2 - Happy Path";
previewStyle = new PreviewStyle( previewStyle = new PreviewStyle(
canvasSize: new( canvasSize: new(
width: 512, width: 1024 / 2, // half the screen size
height: 384 height: 768 / 2 // half the screen size
), ),
canvasStyle: BoxStyle.Empty, canvasStyle: BoxStyle.Empty,
screenStyle: new( screenStyle: new(
@@ -210,37 +239,54 @@ public static class LayoutHelperTests
backgroundStyle: new( backgroundStyle: new(
color1: Color.FromArgb(13, 87, 210), // light blue color1: Color.FromArgb(13, 87, 210), // light blue
color2: Color.FromArgb(3, 68, 192) // darker blue color2: Color.FromArgb(3, 68, 192) // darker blue
) )),
)); extraColors: Array.Empty<Color>());
screens = new List<RectangleInfo> displayInfo = new DisplayInfo(
{ devices: new List<DeviceInfo>
new(0, 0, 1024, 768),
};
activatedLocation = new PointInfo(512, 384);
expectedResult = new PreviewLayout(
virtualScreen: new(0, 0, 1024, 768),
screens: screens,
activatedScreenIndex: 0,
formBounds: new(256, 192, 512, 384),
previewStyle: previewStyle,
previewBounds: new(
outerBounds: new(0, 0, 512, 384),
marginBounds: new(0, 0, 512, 384),
borderBounds: new(0, 0, 512, 384),
paddingBounds: new(0, 0, 512, 384),
contentBounds: new(0, 0, 512, 384)
),
screenshotBounds: new()
{ {
new( new(
outerBounds: new(0, 0, 512, 384), hostname: "localhost",
marginBounds: new(0, 0, 512, 384), localhost: true,
borderBounds: new(1, 1, 510, 382), screens: new List<ScreenInfo>
paddingBounds: new(6, 6, 500, 372), {
contentBounds: new(6, 6, 500, 372) new(
handle: 0,
primary: true,
displayArea: new(0, 0, 1024, 768),
workingArea: new(0, 0, 1024, 768)),
}
), ),
}); });
yield return new object[] { new TestCase(previewStyle, screens, activatedLocation, expectedResult) }; activatedScreen = displayInfo.Devices[0].Screens[0];
activatedLocation = activatedScreen.DisplayArea.Midpoint;
expectedResult = new FormViewModel(
formBounds: new(256, 192, 512, 384),
canvasLayout: new(
canvasBounds: BoxBounds.CreateFromOuterBounds(
outerBounds: new(0, 0, 512, 384),
boxStyle: previewStyle.CanvasStyle),
canvasStyle: previewStyle.CanvasStyle,
deviceLayouts: new List<DeviceViewModel>()
{
new(
deviceInfo: displayInfo.Devices[0],
deviceBounds: BoxBounds.CreateFromOuterBounds(
outerBounds: new(0, 0, 512, 384),
boxStyle: BoxStyle.Empty),
deviceStyle: BoxStyle.Empty,
screenLayouts: new List<ScreenViewModel>
{
new(
screenInfo: displayInfo.Devices[0].Screens[0],
screenBounds: BoxBounds.CreateFromOuterBounds(
outerBounds: new(0, 0, 512, 384),
boxStyle: previewStyle.ScreenStyle),
screenStyle: previewStyle.ScreenStyle),
}
),
}
));
yield return new object[] { new TestCase(testName, previewStyle, displayInfo, activatedScreen, activatedLocation, expectedResult) };
// rounding error check - single screen with 33% scaling, // rounding error check - single screen with 33% scaling,
// no borders, check to make sure form scales to exactly // no borders, check to make sure form scales to exactly
@@ -257,42 +303,61 @@ public static class LayoutHelperTests
// | 0 | // | 0 |
// | | // | |
// +----------------+ // +----------------+
testName = "Test 3 - Rounding Error Check";
previewStyle = new PreviewStyle( previewStyle = new PreviewStyle(
canvasSize: new( canvasSize: new(
width: 300, width: 300,
height: 200 height: 200
), ),
canvasStyle: BoxStyle.Empty, canvasStyle: BoxStyle.Empty,
screenStyle: BoxStyle.Empty); screenStyle: BoxStyle.Empty,
screens = new List<RectangleInfo> extraColors: Array.Empty<Color>());
{ displayInfo = new DisplayInfo(
new(0, 0, 900, 200), devices: new List<DeviceInfo>
};
activatedLocation = new PointInfo(450, 100);
expectedResult = new PreviewLayout(
virtualScreen: new(0, 0, 900, 200),
screens: screens,
activatedScreenIndex: 0,
formBounds: new(300, 66.5m, 300, 67),
previewStyle: previewStyle,
previewBounds: new(
outerBounds: new(0, 0, 300, 67),
marginBounds: new(0, 0, 300, 67),
borderBounds: new(0, 0, 300, 67),
paddingBounds: new(0, 0, 300, 67),
contentBounds: new(0, 0, 300, 67)
),
screenshotBounds: new()
{ {
new( new(
outerBounds: new(0, 0, 300, 67), hostname: "localhost",
marginBounds: new(0, 0, 300, 67), localhost: true,
borderBounds: new(0, 0, 300, 67), screens: new List<ScreenInfo>
paddingBounds: new(0, 0, 300, 67), {
contentBounds: new(0, 0, 300, 67) new(
handle: 0,
primary: true,
displayArea: new(0, 0, 900, 200),
workingArea: new(0, 0, 900, 200)),
}
), ),
}); });
yield return new object[] { new TestCase(previewStyle, screens, activatedLocation, expectedResult) }; activatedScreen = displayInfo.Devices[0].Screens[0];
activatedLocation = activatedScreen.DisplayArea.Midpoint;
expectedResult = new FormViewModel(
formBounds: new(300, 66.5m, 300, 67),
canvasLayout: new(
canvasBounds: BoxBounds.CreateFromOuterBounds(
outerBounds: new(0, 0, 300, 67),
boxStyle: previewStyle.CanvasStyle),
canvasStyle: previewStyle.CanvasStyle,
deviceLayouts: new List<DeviceViewModel>()
{
new(
deviceInfo: displayInfo.Devices[0],
deviceBounds: BoxBounds.CreateFromOuterBounds(
outerBounds: new(0, 0, 300, 67),
boxStyle: BoxStyle.Empty),
deviceStyle: BoxStyle.Empty,
screenLayouts: new List<ScreenViewModel>()
{
new(
screenInfo: displayInfo.Devices[0].Screens[0],
screenBounds: BoxBounds.CreateFromOuterBounds(
outerBounds: new(0, 0, 300, 67),
boxStyle: previewStyle.ScreenStyle),
screenStyle: previewStyle.ScreenStyle),
}
),
}
));
yield return new object[] { new TestCase(testName, previewStyle, displayInfo, activatedScreen, activatedLocation, expectedResult) };
// primary monitor not topmost / leftmost - if there are screens // primary monitor not topmost / leftmost - if there are screens
// that are further left or higher up than the primary monitor // that are further left or higher up than the primary monitor
@@ -306,6 +371,7 @@ public static class LayoutHelperTests
// | 1 | // | 1 |
// | | // | |
// +----------------+ // +----------------+
testName = "Test 4 - Negative Coordinates";
previewStyle = new PreviewStyle( previewStyle = new PreviewStyle(
canvasSize: new( canvasSize: new(
width: 716, width: 716,
@@ -335,49 +401,164 @@ public static class LayoutHelperTests
backgroundStyle: new( backgroundStyle: new(
color1: Color.FromArgb(13, 87, 210), // light blue color1: Color.FromArgb(13, 87, 210), // light blue
color2: Color.FromArgb(3, 68, 192) // darker blue color2: Color.FromArgb(3, 68, 192) // darker blue
) )),
)); extraColors: Array.Empty<Color>());
screens = new List<RectangleInfo> displayInfo = new DisplayInfo(
{ devices: new List<DeviceInfo>
new(-1920, -480, 1920, 1080),
new(0, 0, 5120, 1440),
};
activatedLocation = new(-960, 60);
expectedResult = new PreviewLayout(
virtualScreen: new(-1920, -480, 7040, 1920),
screens: screens,
activatedScreenIndex: 0,
formBounds: new(-1318, -42, 716, 204),
previewStyle: previewStyle,
previewBounds: new(
outerBounds: new(0, 0, 716, 204),
marginBounds: new(0, 0, 716, 204),
borderBounds: new(0, 0, 716, 204),
paddingBounds: new(5, 5, 706, 194),
contentBounds: new(6, 6, 704, 192)
),
screenshotBounds: new()
{ {
new( new(
outerBounds: new(6, 6, 192, 108), hostname: "localhost",
marginBounds: new(6, 6, 192, 108), localhost: true,
borderBounds: new(7, 7, 190, 106), screens: new List<ScreenInfo>
paddingBounds: new(12, 12, 180, 96), {
contentBounds: new(12, 12, 180, 96) new(
), handle: 0,
new( primary: true,
outerBounds: new(198, 54, 512, 144), displayArea: new(-1920, -480, 1920, 1080),
marginBounds: new(198, 54, 512, 144), workingArea: new(-1920, -480, 1920, 1080)),
borderBounds: new(199, 55, 510, 142), new(
paddingBounds: new(204, 60, 500, 132), handle: 0,
contentBounds: new(204, 60, 500, 132) primary: false,
displayArea: new(0, 0, 5120, 1440),
workingArea: new(0, 0, 5120, 1440)),
}
), ),
}); });
yield return new object[] { new TestCase(previewStyle, screens, activatedLocation, expectedResult) }; activatedScreen = displayInfo.Devices[0].Screens[0];
activatedLocation = activatedScreen.DisplayArea.Midpoint;
expectedResult = new FormViewModel(
formBounds: new(-1318, -42, 716, 204),
canvasLayout: new(
canvasBounds: BoxBounds.CreateFromOuterBounds(
outerBounds: new(0, 0, 716, 204),
boxStyle: previewStyle.CanvasStyle),
canvasStyle: previewStyle.CanvasStyle,
deviceLayouts: new List<DeviceViewModel>()
{
new(
deviceInfo: displayInfo.Devices[0],
deviceBounds: BoxBounds.CreateFromOuterBounds(
outerBounds: new(6, 6, 704, 192),
boxStyle: BoxStyle.Empty),
deviceStyle: BoxStyle.Empty,
screenLayouts: new List<ScreenViewModel>()
{
new(
screenInfo: displayInfo.Devices[0].Screens[0],
screenBounds: BoxBounds.CreateFromOuterBounds(
outerBounds: new(6, 6, 192, 108),
boxStyle: previewStyle.ScreenStyle),
screenStyle: previewStyle.ScreenStyle),
new(
screenInfo: displayInfo.Devices[0].Screens[1],
screenBounds: BoxBounds.CreateFromOuterBounds(
outerBounds: new(198, 54, 512, 144),
boxStyle: previewStyle.ScreenStyle),
screenStyle: previewStyle.ScreenStyle),
}
),
}
));
yield return new object[] { new TestCase(testName, previewStyle, displayInfo, activatedScreen, activatedLocation, expectedResult) };
// two devices side-by-side with a single screen each
//
// device 1 device 2
// +----------------+ +----------------+
// | | | |
// | 0 | | 0 |
// | | | |
// +----------------+ +----------------+
testName = "Test 5 - Two Devices Side-by-Side";
previewStyle = new PreviewStyle(
canvasSize: new(
width: 1600,
height: 1200
),
canvasStyle: BoxStyle.Empty,
screenStyle: BoxStyle.Empty,
extraColors: Array.Empty<Color>());
displayInfo = new DisplayInfo(
devices: new List<DeviceInfo>
{
new(
hostname: "localhost",
localhost: true,
screens: new List<ScreenInfo>
{
new(
handle: 0,
primary: true,
displayArea: new(0, 0, 5120, 1440),
workingArea: new(0, 0, 5120, 1440)),
}
),
new(
hostname: "remotehost",
localhost: false,
screens: new List<ScreenInfo>
{
new(
handle: 0,
primary: true,
displayArea: new(0, 0, 5120, 1440),
workingArea: new(0, 0, 5120, 1440)),
}
),
});
activatedScreen = displayInfo.Devices[0].Screens[0];
activatedLocation = activatedScreen.DisplayArea.Midpoint;
expectedResult = new FormViewModel(
formBounds: new(1760, 607.5m, 1600, 225),
canvasLayout: new(
canvasBounds: BoxBounds.CreateFromOuterBounds(
outerBounds: new(0, 0, 1600, 225),
boxStyle: previewStyle.CanvasStyle),
canvasStyle: previewStyle.CanvasStyle,
deviceLayouts: new List<DeviceViewModel>()
{
new(
deviceInfo: displayInfo.Devices[0],
deviceBounds: BoxBounds.CreateFromOuterBounds(
outerBounds: new(0, 0, 800, 225),
boxStyle: BoxStyle.Empty),
deviceStyle: BoxStyle.Empty,
screenLayouts: new List<ScreenViewModel>()
{
new(
screenInfo: displayInfo.Devices[0].Screens[0],
screenBounds: BoxBounds.CreateFromOuterBounds(
outerBounds: new(0, 0, 800, 225),
boxStyle: previewStyle.ScreenStyle),
screenStyle: previewStyle.ScreenStyle),
}
),
new(
deviceInfo: displayInfo.Devices[1],
deviceBounds: BoxBounds.CreateFromOuterBounds(
outerBounds: new(800, 0, 800, 225),
boxStyle: BoxStyle.Empty),
deviceStyle: BoxStyle.Empty,
screenLayouts: new List<ScreenViewModel>()
{
new(
screenInfo: displayInfo.Devices[1].Screens[0],
screenBounds: BoxBounds.CreateFromOuterBounds(
outerBounds: new(800, 0, 800, 225),
boxStyle: previewStyle.ScreenStyle),
screenStyle: previewStyle.ScreenStyle),
}
),
}
));
yield return new object[] { new TestCase(testName, previewStyle, displayInfo, activatedScreen, activatedLocation, expectedResult) };
// TODO: add a test to make sure the form is nudged into the bounds
// of the screen if it's activated near an edge or corner
} }
[TestMethod] [TestMethod]
[DynamicData(nameof(GetTestCases), DynamicDataSourceType.Method)] [DynamicData(nameof(GetTestCases))]
public void RunTestCases(TestCase data) public void RunTestCases(TestCase data)
{ {
// note - even if values are within 0.0001M of each other they could // note - even if values are within 0.0001M of each other they could
@@ -387,117 +568,101 @@ public static class LayoutHelperTests
// (int)1280.000000000000 -> 1280 // (int)1280.000000000000 -> 1280
// so we'll compare the raw values, *and* convert to an int-based // so we'll compare the raw values, *and* convert to an int-based
// Rectangle to compare rounded values // Rectangle to compare rounded values
var actual = LayoutHelper.GetPreviewLayout(data.PreviewStyle, data.Screens, data.ActivatedLocation); var actual = LayoutHelper.GetFormLayout(data.PreviewStyle, data.DisplayInfo, data.ActivatedScreen, data.ActivatedLocation);
var expected = data.ExpectedResult; var expected = data.ExpectedResult;
var options = new JsonSerializerOptions var options = new JsonSerializerOptions
{ {
WriteIndented = true, WriteIndented = true,
}; };
Assert.AreEqual( var actualJson = JsonSerializer.Serialize(actual, options);
JsonSerializer.Serialize(expected, options), var expectedJson = JsonSerializer.Serialize(expected, options);
JsonSerializer.Serialize(actual, options)); Assert.AreEqual(expectedJson, actualJson);
} }
}
[TestClass] /// <summary>
public sealed class GetBoxBoundsFromContentBoundsTests /// Basic performance test just to avoid any massive regressions.
{ /// </summary>
public sealed class TestCase [TestMethod]
[Ignore("Ignore on CI runners - only run locally")]
public void BasicPerformanceTest()
{ {
public TestCase(RectangleInfo contentBounds, BoxStyle boxStyle, BoxBounds expectedResult) // primary monitor not topmost / leftmost - if there are screens
// that are further left or higher up than the primary monitor
// they'll have negative coordinates which has caused some
// issues with calculations in the past. this test will make
// sure we handle screens with negative coordinates gracefully
//
// +-------+
// | 0 +----------------+
// +-------+ |
// | 1 |
// | |
// +----------------+
var previewStyle = new PreviewStyle(
canvasSize: new(
width: 716,
height: 204
),
canvasStyle: new(
marginStyle: MarginStyle.Empty,
borderStyle: new(
color: SystemColors.Highlight,
all: 5,
depth: 3),
paddingStyle: new(
all: 1),
backgroundStyle: new(
color1: Color.FromArgb(13, 87, 210), // light blue
color2: Color.FromArgb(3, 68, 192) // darker blue
)
),
screenStyle: new(
marginStyle: new(
all: 1),
borderStyle: new(
color: SystemColors.Highlight,
all: 5,
depth: 3),
paddingStyle: PaddingStyle.Empty,
backgroundStyle: new(
color1: Color.FromArgb(13, 87, 210), // light blue
color2: Color.FromArgb(3, 68, 192) // darker blue
)),
extraColors: Array.Empty<Color>());
var displayInfo = new DisplayInfo(
devices: new List<DeviceInfo>
{
new(
hostname: "localhost",
localhost: true,
screens: new List<ScreenInfo>
{
new(
handle: 0,
primary: true,
displayArea: new(-1920, -480, 1920, 1080),
workingArea: new(-1920, -480, 1920, 1080)),
new(
handle: 0,
primary: false,
displayArea: new(0, 0, 5120, 1440),
workingArea: new(0, 0, 5120, 1440)),
}
),
});
var activatedScreen = displayInfo.Devices[0].Screens[0];
var activatedLocation = activatedScreen.DisplayArea.Midpoint;
var timer = Stopwatch.StartNew();
for (var i = 0; i < 10_000; i++)
{ {
this.ContentBounds = contentBounds; var formLayout = LayoutHelper.GetFormLayout(previewStyle, displayInfo, activatedScreen, activatedLocation);
this.BoxStyle = boxStyle;
this.ExpectedResult = expectedResult;
} }
public RectangleInfo ContentBounds { get; set; } timer.Stop();
public BoxStyle BoxStyle { get; set; } // runs on my machine in about 180-200ms, so leave a bit of headroom
Assert.IsTrue(timer.ElapsedMilliseconds < 225);
public BoxBounds ExpectedResult { get; set; }
}
public static IEnumerable<object[]> GetTestCases()
{
yield return new[]
{
new TestCase(
contentBounds: new(100, 100, 800, 600),
boxStyle: new(
marginStyle: new(3),
borderStyle: new(Color.Red, 5, 0),
paddingStyle: new(7),
backgroundStyle: BackgroundStyle.Empty),
expectedResult: new(
outerBounds: new(85, 85, 830, 630),
marginBounds: new(85, 85, 830, 630),
borderBounds: new(88, 88, 824, 624),
paddingBounds: new(93, 93, 814, 614),
contentBounds: new(100, 100, 800, 600))),
};
}
[TestMethod]
[DynamicData(nameof(GetTestCases), DynamicDataSourceType.Method)]
public void RunTestCases(TestCase data)
{
var actual = LayoutHelper.GetBoxBoundsFromContentBounds(data.ContentBounds, data.BoxStyle);
var expected = data.ExpectedResult;
Assert.AreEqual(
JsonSerializer.Serialize(expected),
JsonSerializer.Serialize(actual));
}
}
[TestClass]
public sealed class GetBoxBoundsFromOuterBoundsTests
{
public sealed class TestCase
{
public TestCase(RectangleInfo outerBounds, BoxStyle boxStyle, BoxBounds expectedResult)
{
this.OuterBounds = outerBounds;
this.BoxStyle = boxStyle;
this.ExpectedResult = expectedResult;
}
public RectangleInfo OuterBounds { get; set; }
public BoxStyle BoxStyle { get; set; }
public BoxBounds ExpectedResult { get; set; }
}
public static IEnumerable<object[]> GetTestCases()
{
yield return new[]
{
new TestCase(
outerBounds: new(85, 85, 830, 630),
boxStyle: new(
marginStyle: new(3),
borderStyle: new(Color.Red, 5, 0),
paddingStyle: new(7),
backgroundStyle: BackgroundStyle.Empty),
expectedResult: new(
outerBounds: new(85, 85, 830, 630),
marginBounds: new(85, 85, 830, 630),
borderBounds: new(88, 88, 824, 624),
paddingBounds: new(93, 93, 814, 614),
contentBounds: new(100, 100, 800, 600))),
};
}
[TestMethod]
[DynamicData(nameof(GetTestCases), DynamicDataSourceType.Method)]
public void RunTestCases(TestCase data)
{
var actual = LayoutHelper.GetBoxBoundsFromOuterBounds(data.OuterBounds, data.BoxStyle);
var expected = data.ExpectedResult;
Assert.AreEqual(
JsonSerializer.Serialize(expected),
JsonSerializer.Serialize(actual));
} }
} }
} }

View File

@@ -3,12 +3,12 @@
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.VisualStudio.TestTools.UnitTesting;
using MouseJump.Common.Helpers; using MouseJump.Common.Helpers;
using MouseJump.Common.Models.Drawing; using MouseJump.Models.Drawing;
namespace MouseJump.Common.UnitTests.Helpers; namespace MouseJump.Common.UnitTests.Helpers;
[TestClass]
public static class MouseHelperTests public static class MouseHelperTests
{ {
[TestClass] [TestClass]
@@ -58,7 +58,7 @@ public static class MouseHelperTests
} }
[TestMethod] [TestMethod]
[DynamicData(nameof(GetTestCases), DynamicDataSourceType.Method)] [DynamicData(nameof(GetTestCases))]
public void RunTestCases(TestCase data) public void RunTestCases(TestCase data)
{ {
var actual = MouseHelper.GetJumpLocation( var actual = MouseHelper.GetJumpLocation(

Binary file not shown.

Before

Width:  |  Height:  |  Size: 202 KiB

After

Width:  |  Height:  |  Size: 202 KiB

View File

@@ -1,44 +1,40 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<!-- Look at Directory.Build.props in root for common stuff as well --> <!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="$(RepoRoot)src\Common.Dotnet.CsWinRT.props" /> <Import Project="$(RepoRoot)src\Common.Dotnet.CsWinRT.props" />
<PropertyGroup> <PropertyGroup>
<ProjectGuid>{D5E42C63-57C5-4EF6-AECE-1E2FCA725B77}</ProjectGuid>
<AssemblyName>PowerToys.MouseJump.Common.UnitTests</AssemblyName>
<AssemblyTitle>PowerToys.MouseJump.Common.UnitTests</AssemblyTitle>
<AssemblyDescription>PowerToys MouseJump.Common.UnitTests</AssemblyDescription>
<OutputType>Library</OutputType>
<OutputPath>..\..\..\..\$(Platform)\$(Configuration)\tests\MouseJump.Common.UnitTests\</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<ProjectGuid>{D5E42C63-57C5-4EF6-AECE-1E2FCA725B77}</ProjectGuid> <Nullable>enable</Nullable>
<AssemblyName>PowerToys.MouseJump.Common.UnitTests</AssemblyName> <ImplicitUsings>enable</ImplicitUsings>
<AssemblyTitle>PowerToys.MouseJump.Common.UnitTests</AssemblyTitle> </PropertyGroup>
<AssemblyDescription>PowerToys MouseJump.Common.UnitTests</AssemblyDescription>
<OutputType>Exe</OutputType>
<OutputPath>$(RepoRoot)$(Platform)\$(Configuration)\tests\MouseJump.Common.UnitTests\</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<Nullable>enable</Nullable> <ItemGroup>
<UseWindowsForms>true</UseWindowsForms> <None Remove="Helpers\_test-4grid-desktop.png" />
<ImplicitUsings>enable</ImplicitUsings> <None Remove="Helpers\_test-4grid-expected.png" />
<None Remove="Helpers\_test-win11-desktop.png" />
<None Remove="Helpers\_test-win11-expected.png" />
</ItemGroup>
</PropertyGroup> <ItemGroup>
<EmbeddedResource Include="Helpers\_test-4grid-desktop.png" />
<EmbeddedResource Include="Helpers\_test-4grid-expected.png" />
<EmbeddedResource Include="Helpers\_test-win11-desktop.png" />
<EmbeddedResource Include="Helpers\_test-win11-expected.png" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<None Remove="Helpers\_test-4grid-desktop.png" /> <ProjectReference Include="..\MouseJump.Common\MouseJump.Common.csproj" />
<None Remove="Helpers\_test-4grid-expected.png" /> </ItemGroup>
<None Remove="Helpers\_test-win11-desktop.png" />
<None Remove="Helpers\_test-win11-expected.png" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Helpers\_test-4grid-desktop.png" />
<EmbeddedResource Include="Helpers\_test-4grid-expected.png" />
<EmbeddedResource Include="Helpers\_test-win11-desktop.png" />
<EmbeddedResource Include="Helpers\_test-win11-expected.png" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="MSTest" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MouseJump.Common\MouseJump.Common.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="MSTest" />
</ItemGroup>
</Project> </Project>

View File

@@ -1,12 +1,13 @@
// Copyright (c) Microsoft Corporation // Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System.Drawing;
using System.Globalization; using System.Globalization;
namespace MouseJump.Common.Helpers; namespace MouseJump.Common.Helpers;
public static class ConfigHelper public static class ColorHelper
{ {
public static Color? ToUnnamedColor(Color? value) public static Color? ToUnnamedColor(Color? value)
{ {

View File

@@ -0,0 +1,37 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using MouseJump.Models.Display;
using MouseJump.Models.Drawing;
namespace MouseJump.Common.Helpers;
public static class DeviceHelper
{
public static DisplayInfo GetDisplayInfo()
{
var devices = new List<DeviceInfo>();
// add the local devices
devices.Add(
new(
hostname: Environment.MachineName,
localhost: true,
screens: ScreenHelper.GetAllScreens()));
return new DisplayInfo(
devices: devices);
}
public static ScreenInfo GetActivatedScreen(DeviceInfo deviceInfo, PointInfo activatedLocation)
{
ArgumentNullException.ThrowIfNull(deviceInfo);
ArgumentNullException.ThrowIfNull(activatedLocation);
var activatedScreen = deviceInfo.Screens
.Single(screen => screen.DisplayArea.Contains(activatedLocation));
return activatedScreen;
}
}

View File

@@ -0,0 +1,55 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Runtime.Versioning;
using Windows.Win32;
using Windows.Win32.UI.HiDpi;
namespace MouseJump.Common.Helpers;
/// <summary>
/// Based on WinForms Application.HighDpiMode
/// see https://github.com/dotnet/winforms/blob/1c324d074280ab5de6342d973069faa687f2c165/src/System.Windows.Forms/System/Windows/Forms/Application.cs#L18
/// https://github.com/dotnet/winforms/blob/1c324d074280ab5de6342d973069faa687f2c165/src/System.Windows.Forms.Primitives/src/System/Windows/Forms/Internals/ScaleHelper.cs#L14
/// </summary>
public static class DpiModeHelper
{
/// <summary>
/// See https://github.com/dotnet/winforms/blob/bd91bfb26ce90ac31e950c01dcb2b6e0776453a7/src/System.Private.Windows.Core/src/System/Private/Windows/OsVersion.cs#L9
/// </summary>
private static bool IsWindows10_1607OrGreater()
=> OperatingSystem.IsWindowsVersionAtLeast(major: 10, build: 14393);
/// <summary>
/// Ensures that the application is running in per-monitor v2 DPI awareness mode.
/// </summary>
/// <remarks>
/// See https://github.com/dotnet/winforms/blob/bd91bfb26ce90ac31e950c01dcb2b6e0776453a7/src/System.Windows.Forms.Primitives/src/System/Windows/Forms/Internals/ScaleHelper.cs#L368
/// </remarks>
[SupportedOSPlatform("windows10.0.14393")]
public static void EnsurePerMonitorV2Enabled()
{
// PowerToys supports the following operating systems:
//
// * Windows 11 or Windows 10 version 2004 (code name 20H1 / build number 19041) or newer.
//
// so we'll do the same(ish)
if (!DpiModeHelper.IsWindows10_1607OrGreater())
{
throw new PlatformNotSupportedException(
"Windows 10 version1607 or higher is required to use this application.");
}
// there's a weird problem where AreDpiAwarenessContextsEqual was returning TRUE in debug mode
// but FALSE in release mode and i couldn't work out why, so we can't do it the *right* way.
// we'll just use GetAwarenessFromDpiAwarenessContext instead as a near-enough workaround.
var dpiAwarenessContext = PInvoke.GetThreadDpiAwarenessContext();
var dpiAwareness = PInvoke.GetAwarenessFromDpiAwarenessContext(dpiAwarenessContext);
if (dpiAwareness != DPI_AWARENESS.DPI_AWARENESS_PER_MONITOR_AWARE)
{
throw new InvalidOperationException($"high dpi mode is not set to {nameof(DPI_AWARENESS.DPI_AWARENESS_PER_MONITOR_AWARE)}");
}
}
}

View File

@@ -3,62 +3,101 @@
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System.Diagnostics; using System.Diagnostics;
using System.Drawing;
using System.Drawing.Drawing2D; using System.Drawing.Drawing2D;
using System.Drawing.Imaging; using System.Drawing.Imaging;
using MouseJump.Common.Imaging; using MouseJump.Common.Imaging;
using MouseJump.Common.Models.Drawing; using MouseJump.Models.Display;
using MouseJump.Common.Models.Layout; using MouseJump.Models.Drawing;
using MouseJump.Common.Models.Styles; using MouseJump.Models.Styles;
using MouseJump.Models.ViewModel;
namespace MouseJump.Common.Helpers; namespace MouseJump.Common.Helpers;
public static class DrawingHelper public static class DrawingHelper
{ {
public static Bitmap RenderPreview( /// <summary>
PreviewLayout previewLayout, /// Renders a preview image of the specified canvas layout.
IImageRegionCopyService imageCopyService, /// </summary>
Action<Bitmap>? previewImageCreatedCallback = null, /// <param name="canvasLayout">
Action? previewImageUpdatedCallback = null) /// The layout of the canvas, including the layout of all devices and screens.
/// </param>
/// <param name="activatedScreen">
/// The screen that is currently activated (i.e. the one that the user is interacting with).
/// </param>
/// <param name="imageRegionCopyServices">
/// A list of IImageRegionCopyService implementations, one for each device in the canvas layout.
/// </param>
/// <param name="previewImageCreatedCallback">
/// A callback that is invoked when the preview image is created.
/// </param>
/// <param name="previewImageUpdatedCallback">
/// A callback that is invoked when the preview image is updated.
/// </param>
/// <returns>
/// A preview image of the canvas layout.
/// </returns>
public static async Task<Bitmap> RenderPreviewAsync(
CanvasViewModel canvasLayout,
ScreenInfo activatedScreen,
List<IImageRegionCopyService> imageRegionCopyServices,
Func<Bitmap, Task>? previewImageCreatedCallback = null,
Func<Bitmap, Task>? previewImageUpdatedCallback = null)
{ {
var stopwatch = Stopwatch.StartNew(); var stopwatch = Stopwatch.StartNew();
// initialize the preview image // initialize the preview image
var previewBounds = previewLayout.PreviewBounds.OuterBounds.ToRectangle(); var previewBounds = canvasLayout.CanvasBounds.OuterBounds.ToRectangle();
var previewImage = new Bitmap(previewBounds.Width, previewBounds.Height, PixelFormat.Format32bppPArgb); var previewImage = new Bitmap(previewBounds.Width, previewBounds.Height, PixelFormat.Format32bppPArgb);
var previewGraphics = Graphics.FromImage(previewImage); using var previewGraphics = Graphics.FromImage(previewImage);
previewImageCreatedCallback?.Invoke(previewImage); if (previewImageCreatedCallback != null)
{
await previewImageCreatedCallback(previewImage);
}
DrawingHelper.DrawRaisedBorder(previewGraphics, previewLayout.PreviewStyle.CanvasStyle, previewLayout.PreviewBounds); DrawingHelper.DrawRaisedBorder(previewGraphics, canvasLayout.CanvasBounds, canvasLayout.CanvasStyle);
DrawingHelper.DrawBackgroundFill( DrawingHelper.DrawBackgroundFill(
previewGraphics, previewGraphics,
previewLayout.PreviewStyle.CanvasStyle, canvasLayout.CanvasStyle,
previewLayout.PreviewBounds, canvasLayout.CanvasBounds,
[]); []);
// sort the source and target screen areas into the order we want to // sort the source and target screen areas into the order we want to
// draw them, putting the activated screen first (we need to capture // draw them, putting the activated screen first (we need to capture
// and draw the activated screen before we show the form because // and draw the activated screen before we show the form because
// otherwise we'll capture the form as part of the screenshot!) // otherwise we'll capture the form as part of the screenshot!)
var sourceScreens = new List<RectangleInfo> { previewLayout.Screens[previewLayout.ActivatedScreenIndex] } var screenDrawingOps = canvasLayout.DeviceLayouts
.Concat(previewLayout.Screens.Where((_, idx) => idx != previewLayout.ActivatedScreenIndex)) .SelectMany(
.ToList(); (deviceLayout, deviceIndex) => deviceLayout.ScreenLayouts.Select(
var targetScreens = new List<BoxBounds> { previewLayout.ScreenshotBounds[previewLayout.ActivatedScreenIndex] } screenLayout => new
.Concat(previewLayout.ScreenshotBounds.Where((_, idx) => idx != previewLayout.ActivatedScreenIndex)) {
DeviceIndex = deviceIndex,
DeviceLayout = deviceLayout,
ScreenLayout = screenLayout,
CopyService = imageRegionCopyServices[deviceIndex],
}))
.OrderByDescending(
pair => object.ReferenceEquals(pair.ScreenLayout.ScreenInfo, activatedScreen))
.ToList(); .ToList();
// draw all the screenshot bezels // draw all the screenshot bezels
foreach (var screenshotBounds in previewLayout.ScreenshotBounds) foreach (var screenDrawingOp in screenDrawingOps)
{ {
DrawingHelper.DrawRaisedBorder( DrawingHelper.DrawRaisedBorder(
previewGraphics, previewLayout.PreviewStyle.ScreenStyle, screenshotBounds); previewGraphics, screenDrawingOp.ScreenLayout.ScreenBounds, screenDrawingOp.ScreenLayout.ScreenStyle);
} }
var refreshRequired = false; var refreshRequired = false;
var placeholdersDrawn = false; var placeholdersDrawn = false;
for (var i = 0; i < sourceScreens.Count; i++) for (var i = 0; i < screenDrawingOps.Count; i++)
{ {
imageCopyService.CopyImageRegion(previewGraphics, sourceScreens[i], targetScreens[i].ContentBounds); var screenDrawingOp = screenDrawingOps[i];
screenDrawingOp.CopyService.CopyImageRegion(
targetGraphics: previewGraphics,
sourceBounds: screenDrawingOp.ScreenLayout.ScreenInfo.DisplayArea,
targetBounds: screenDrawingOp.ScreenLayout.ScreenBounds.ContentBounds);
refreshRequired = true; refreshRequired = true;
// show the placeholder images and show the form if it looks like it might take // show the placeholder images and show the form if it looks like it might take
@@ -70,19 +109,26 @@ public static class DrawingHelper
{ {
DrawingHelper.DrawScreenPlaceholders( DrawingHelper.DrawScreenPlaceholders(
previewGraphics, previewGraphics,
previewLayout.PreviewStyle.ScreenStyle, screenDrawingOp.ScreenLayout.ScreenStyle,
targetScreens.GetRange(i + 1, targetScreens.Count - i - 1)); screenDrawingOps
.Skip(i + 1)
.Select(drawingOp => drawingOp.ScreenLayout.ScreenBounds)
.ToList());
placeholdersDrawn = true; placeholdersDrawn = true;
} }
previewImageUpdatedCallback?.Invoke(); if (previewImageUpdatedCallback is not null)
{
await previewImageUpdatedCallback(previewImage);
}
refreshRequired = false; refreshRequired = false;
} }
} }
if (refreshRequired) if (refreshRequired && (previewImageUpdatedCallback is not null))
{ {
previewImageUpdatedCallback?.Invoke(); await previewImageUpdatedCallback(previewImage);
} }
stopwatch.Stop(); stopwatch.Stop();
@@ -94,10 +140,10 @@ public static class DrawingHelper
/// Draws a border shape with an optional raised 3d highlight and shadow effect. /// Draws a border shape with an optional raised 3d highlight and shadow effect.
/// </summary> /// </summary>
private static void DrawRaisedBorder( private static void DrawRaisedBorder(
Graphics graphics, BoxStyle boxStyle, BoxBounds boxBounds) Graphics graphics, BoxBounds boxBounds, BoxStyle boxStyle)
{ {
var borderStyle = boxStyle.BorderStyle; var borderStyle = boxStyle.BorderStyle;
if ((borderStyle.Horizontal == 0) || (borderStyle.Vertical == 0)) if ((borderStyle.Horizontal < 1) || (borderStyle.Vertical < 1))
{ {
return; return;
} }
@@ -109,7 +155,7 @@ public static class DrawingHelper
// draw the main box border // draw the main box border
using var borderBrush = new SolidBrush(borderStyle.Color.Value); using var borderBrush = new SolidBrush(borderStyle.Color.Value);
var borderRegion = new Region(boxBounds.BorderBounds.ToRectangle()); using var borderRegion = new Region(boxBounds.BorderBounds.ToRectangle());
borderRegion.Exclude(boxBounds.PaddingBounds.ToRectangle()); borderRegion.Exclude(boxBounds.PaddingBounds.ToRectangle());
graphics.FillRegion(borderBrush, borderRegion); graphics.FillRegion(borderBrush, borderRegion);
@@ -195,7 +241,7 @@ public static class DrawingHelper
// it's faster to build a region with the screen areas excluded // it's faster to build a region with the screen areas excluded
// and fill that than it is to fill the entire bounding rectangle // and fill that than it is to fill the entire bounding rectangle
var backgroundRegion = new Region(backgroundBounds.ToRectangle()); using var backgroundRegion = new Region(backgroundBounds.ToRectangle());
foreach (var exclude in excludeBounds) foreach (var exclude in excludeBounds)
{ {
backgroundRegion.Exclude(exclude.ToRectangle()); backgroundRegion.Exclude(exclude.ToRectangle());
@@ -208,14 +254,14 @@ public static class DrawingHelper
/// Draws placeholder background images for the specified screens on the preview. /// Draws placeholder background images for the specified screens on the preview.
/// </summary> /// </summary>
private static void DrawScreenPlaceholders( private static void DrawScreenPlaceholders(
Graphics graphics, BoxStyle screenStyle, IList<BoxBounds> screenBounds) Graphics graphics, BoxStyle screenStyle, List<BoxBounds> screenBounds)
{ {
if (screenBounds.Count == 0) if (screenBounds.Count == 0)
{ {
return; return;
} }
if (screenStyle?.BackgroundStyle?.Color1 == null) if (screenStyle.BackgroundStyle.Color1 == null)
{ {
return; return;
} }

View File

@@ -2,152 +2,401 @@
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using MouseJump.Common.Models.Drawing; using System.Diagnostics;
using MouseJump.Common.Models.Layout; using System.Drawing;
using MouseJump.Common.Models.Styles;
using MouseJump.Models.Display;
using MouseJump.Models.Drawing;
using MouseJump.Models.Styles;
using MouseJump.Models.ViewModel;
namespace MouseJump.Common.Helpers; namespace MouseJump.Common.Helpers;
public static class LayoutHelper public static class LayoutHelper
{ {
public static PreviewLayout GetPreviewLayout( public static RectangleInfo GetCombinedScreenBounds(List<RectangleInfo> screenBounds)
PreviewStyle previewStyle, List<RectangleInfo> screens, PointInfo activatedLocation) {
return screenBounds.Skip(1).Aggregate(
seed: screenBounds.First(),
(combined, screenBounds) => combined.Union(screenBounds));
}
public static FormViewModel GetFormLayout(
PreviewStyle previewStyle, DisplayInfo displayInfo, ScreenInfo activatedScreen, PointInfo activatedLocation)
{ {
ArgumentNullException.ThrowIfNull(previewStyle); ArgumentNullException.ThrowIfNull(previewStyle);
ArgumentNullException.ThrowIfNull(screens); ArgumentNullException.ThrowIfNull(displayInfo);
ArgumentNullException.ThrowIfNull(activatedScreen);
ArgumentNullException.ThrowIfNull(activatedLocation);
if (screens.Count == 0) /*
example layout:
+-------------------------------[form]------------------------------+
|▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒[canvas]▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒|
|▒▒+----------------------------[grid]----+----------------------+▒▒|
|▒▒| |▓▓▓▓▓▓[device 2]▓▓▓▓▓▓|▒▒|
|▒▒| |▓▓░░░░[screen 1]░░░░▓▓|▒▒|
|▒▒|▓▓▓▓▓▓▓▓▓▓▓▓▓▓[device 1]▓▓▓▓▓▓▓▓▓▓▓▓▓▓|▓▓░░ ░░▓▓|▒▒|
|▒▒|▓▓░░░░[screen 1]░░░░░░[screen 2]░░░░▓▓|▓▓░░ ░░▓▓|▒▒|
|▒▒|▓▓░░ ░░ ░░▓▓|▓▓░░ ░░▓▓|▒▒|
|▒▒|▓▓░░ ░░ ░░▓▓|▓▓░░░░░░░░░░░░░░░░░░▓▓|▒▒|
|▒▒|▓▓░░ ░░ ░░▓▓|▓▓░░░░[screen 2]░░░░▓▓|▒▒|
|▒▒|▓▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓▓|▓▓░░ ░░▓▓|▒▒|
|▒▒|▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓|▓▓░░ ░░▓▓|▒▒|
|▒▒| |▓▓░░ ░░▓▓|▒▒|
|▒▒| |▓▓░░░░░░░░░░░░░░░░░░▓▓|▒▒|
|▒▒| |▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓|▒▒|
|▒▒+--------------------------------------+----------------------+▒▒|
|▒▒| ▓▓▓▓▓▓[device 3]▓▓▓▓▓▓ | |▒▒|
|▒▒| ▓▓░░░░[screen 1]░░░░▓▓ | |▒▒|
|▒▒| ▓▓░░ ░░▓▓ | |▒▒|
|▒▒| ▓▓░░ ░░▓▓ | ▓▓▓[device 4]▓▓▓ |▒▒|
|▒▒| ▓▓░░ ░░▓▓ | ▓▓░[screen 1]░▓▓ |▒▒|
|▒▒| ▓▓░░░░░░░░░░░░░░░░░░▓▓ | ▓▓░░ ░░▓▓ |▒▒|
|▒▒| ▓▓░░░░[screen 2]░░░░▓▓ | ▓▓░░ ░░▓▓ |▒▒|
|▒▒| ▓▓░░ ░░▓▓ | ▓▓░░░░░░░░░░░░▓▓ |▒▒|
|▒▒| ▓▓░░ ░░▓▓ | ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ |▒▒|
|▒▒| ▓▓░░ ░░▓▓ | |▒▒|
|▒▒| ▓▓░░░░░░░░░░░░░░░░░░▓▓ | |▒▒|
|▒▒| ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ | |▒▒|
|▒▒+--------------------------------------+----------------------+▒▒|
|▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒|
+-------------------------------------------------------------------+
*/
// arrange the form, canvas, devices and screens
var formLayout = LayoutHelper.CreateInitialFormLayout(previewStyle, displayInfo, activatedScreen);
LayoutHelper.ArrangeAndScaleDeviceLayouts(formLayout);
LayoutHelper.ArrangeAndScaleScreenLayouts(formLayout);
LayoutHelper.ArrangeAndResizeCanvasLayout(formLayout);
LayoutHelper.ArrangeAndResizeFormLayout(formLayout, activatedScreen, activatedLocation);
return formLayout.Build();
}
internal static FormViewModel.Builder CreateInitialFormLayout(
PreviewStyle previewStyle, DisplayInfo displayInfo, ScreenInfo activatedScreen)
{
ArgumentNullException.ThrowIfNull(previewStyle);
ArgumentNullException.ThrowIfNull(displayInfo);
// check we have at least one device
if (displayInfo.Devices.Count == 0)
{ {
throw new ArgumentException("Value must contain at least one item.", nameof(screens)); throw new ArgumentException("Value must contain at least one device.", nameof(displayInfo));
} }
var builder = new PreviewLayout.Builder(); /*
builder.Screens = screens.ToList(); // check each device has at least one screen.
for (var deviceIndex = 0; deviceIndex < displayInfo.Devices.Count; deviceIndex++)
// calculate the bounding rectangle for the virtual screen {
builder.VirtualScreen = LayoutHelper.GetCombinedScreenBounds(builder.Screens); var device = displayInfo.Devices[deviceIndex];
if (device.Screens.Count == 0)
// find the screen that contains the activated location - this is the {
// one we'll show the preview form on throw new ArgumentException($"{nameof(displayInfo)}.{nameof(displayInfo.Devices)}[{deviceIndex}] must contain at least one screen.", nameof(displayInfo));
var activatedScreen = builder.Screens.Single( }
screen => screen.Contains(activatedLocation)); }
builder.ActivatedScreenIndex = builder.Screens.IndexOf(activatedScreen); */
// work out the maximum allowed size of the preview form: // work out the maximum allowed size of the preview form:
// * can't be bigger than the activated screen // * can't be bigger than the activated screen
// * can't be bigger than the configured canvas size // * can't be bigger than the configured canvas size
var maxPreviewSize = activatedScreen.Size var formMaxBounds = new RectangleInfo(
.Intersect(previewStyle.CanvasSize); previewStyle.CanvasSize
.Clamp(activatedScreen.DisplayArea.Size));
// the "content area" (i.e. drawing area) for screenshots is inside the var screenStyles = LayoutHelper.GetDeviceScreenStyles(previewStyle, displayInfo.Devices.Count).ToList();
// preview border and inside the preview padding (if any)
var maxContentSize = maxPreviewSize
.Shrink(previewStyle.CanvasStyle.MarginStyle)
.Shrink(previewStyle.CanvasStyle.BorderStyle)
.Shrink(previewStyle.CanvasStyle.PaddingStyle);
// work out the actual size of the "content area" by scaling the virtual screen // create an initial form layout.
// to fit inside the maximum content area while maintaining its aspect ration. // this is a nested structure of mutable "builder" objects that can be used to
// we'll also offset it to allow for any margins, borders and padding // build the final immutable layout objects once all the bounds have been calculated
var contentBounds = builder.VirtualScreen.Size var formLayout = new FormViewModel.Builder
.ScaleToFit(maxContentSize, out var scalingRatio) {
.Round() FormBounds = RectangleInfo.Empty,
.Clamp(maxContentSize) CanvasLayout = new()
.PlaceAt(0, 0) {
.Offset(previewStyle.CanvasStyle.MarginStyle.Left, previewStyle.CanvasStyle.MarginStyle.Top) CanvasBounds = BoxBounds.CreateFromOuterBounds(
.Offset(previewStyle.CanvasStyle.BorderStyle.Left, previewStyle.CanvasStyle.BorderStyle.Top) outerBounds: formMaxBounds,
.Offset(previewStyle.CanvasStyle.PaddingStyle.Left, previewStyle.CanvasStyle.PaddingStyle.Top); boxStyle: previewStyle.CanvasStyle),
CanvasStyle = previewStyle.CanvasStyle,
DeviceLayouts = displayInfo.Devices.Select(
(deviceInfo, deviceIndex) => new DeviceViewModel.Builder
{
DeviceInfo = deviceInfo,
DeviceBounds = BoxBounds.Empty,
DeviceStyle = BoxStyle.Empty,
ScreenLayouts = deviceInfo.Screens.Select(
screenInfo => new ScreenViewModel.Builder
{
ScreenInfo = screenInfo,
ScreenBounds = BoxBounds.Empty,
ScreenStyle = screenStyles[deviceIndex],
}).ToList(),
}).ToList(),
},
};
// now we know the actual size of the content area we can work outwards to return formLayout;
// get the size of the background bounds including margins, borders and padding }
builder.PreviewStyle = previewStyle;
builder.PreviewBounds = LayoutHelper.GetBoxBoundsFromContentBounds(
contentBounds,
previewStyle.CanvasStyle);
// ... and then the size and position of the preview form on the activated screen internal static IEnumerable<BoxStyle> GetDeviceScreenStyles(PreviewStyle previewStyle, int screenCount)
// * center the form to the activated position, but nudge it back {
// inside the visible area of the activated screen if it falls outside // work out the colors to use for the screen borders for each device
var formBounds = builder.PreviewBounds.OuterBounds // (add the default localhost color first and then loop over the extra colors until we've got enough)
.Center(activatedLocation) var screenBorderColors = new List<Color?> { previewStyle.ScreenStyle.BorderStyle.Color };
.Clamp(activatedScreen); while (screenBorderColors.Count < screenCount)
builder.FormBounds = formBounds; {
if (previewStyle.ExtraColors?.Count > 0)
{
screenBorderColors.AddRange(previewStyle.ExtraColors.Cast<Color?>());
continue;
}
// now calculate the positions of each of the screenshot images on the preview screenBorderColors.Add(previewStyle.ScreenStyle.BorderStyle.Color);
builder.ScreenshotBounds = builder.Screens }
// convert the colors into screen styles
// note - only create a new ScreenStyle if the border color is different.
// (this is to preserve a reference to ScreenStyle.Empty - and
// "ScreenStyle.IsEmpty == true" - if the new border color is Transparent)
return screenBorderColors
.Take(screenCount)
.Select( .Select(
screen => LayoutHelper.GetBoxBoundsFromOuterBounds( color => previewStyle.ScreenStyle.BorderStyle.Color == color
screen ? previewStyle.ScreenStyle
.Offset(builder.VirtualScreen.Location.ToSize().Invert()) : new BoxStyle(
.Scale(scalingRatio) previewStyle.ScreenStyle.MarginStyle,
.Offset(builder.PreviewBounds.ContentBounds.Location.ToSize()) previewStyle.ScreenStyle.BorderStyle.WithColor(color),
.Round(), previewStyle.ScreenStyle.PaddingStyle,
previewStyle.ScreenStyle)) previewStyle.ScreenStyle.BackgroundStyle));
.ToList();
return builder.Build();
}
public static RectangleInfo GetCombinedScreenBounds(List<RectangleInfo> screens)
{
return screens.Skip(1).Aggregate(
seed: screens.First(),
(bounds, screen) => bounds.Union(screen));
} }
/// <summary> /// <summary>
/// Calculates the bounds of the various areas of a box, given the content bounds and the box style. /// Arranges the device layouts into a non-overlapping grid and scales them to fit inside the specified content bounds.
/// Starts with the content bounds and works outward, enlarging the content bounds by the padding, border, and margin sizes to calculate the outer bounds of the box.
/// </summary> /// </summary>
/// <param name="contentBounds">The content bounds of the box.</param> internal static void ArrangeAndScaleDeviceLayouts(FormViewModel.Builder formLayout)
/// <param name="boxStyle">The style of the box, which includes the sizes of the margin, border, and padding areas.</param>
/// <returns>A <see cref="BoxBounds"/> object that represents the bounds of the different areas of the box.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="contentBounds"/> or <paramref name="boxStyle"/> is null.</exception>
/// <exception cref="ArgumentException">Thrown when any of the styles in <paramref name="boxStyle"/> is null.</exception>
public static BoxBounds GetBoxBoundsFromContentBounds(
RectangleInfo contentBounds,
BoxStyle boxStyle)
{ {
ArgumentNullException.ThrowIfNull(contentBounds); var deviceLayouts = formLayout.CanvasLayout?.DeviceLayouts
ArgumentNullException.ThrowIfNull(boxStyle); ?? throw new InvalidOperationException();
if (boxStyle.PaddingStyle == null || boxStyle.BorderStyle == null || boxStyle.MarginStyle == null) var contentBounds = formLayout.CanvasLayout.CanvasBounds?.ContentBounds
?? throw new InvalidOperationException();
// build an initial grid of devices at 100% scale. the grid is currently a single row
// of cells high with all the devices arranged left to right for the time being.
// we'll enhance this to allow for multiple rows later to support the
// Mouse Without Borders "square" arrangement.
var gridRowCount = 1;
var gridColumnCount = deviceLayouts.Count;
var deviceGrid = new DeviceViewModel.Builder[gridRowCount, gridColumnCount];
for (var columnIndex = 0; columnIndex < gridColumnCount; columnIndex++)
{ {
throw new ArgumentException(null, nameof(boxStyle)); deviceGrid[0, columnIndex] = deviceLayouts[columnIndex];
} }
var paddingBounds = contentBounds.Enlarge(boxStyle.PaddingStyle); // if a device has zero screens (or if we failed to connect and read screen layouts) we
var borderBounds = paddingBounds.Enlarge(boxStyle.BorderStyle); // still want to allocate space in the full size grid so that it doesn't disappear into
var marginBounds = borderBounds.Enlarge(boxStyle.MarginStyle); // a zero width or height cell.
var outerBounds = marginBounds; var fullSizeGridCellMinSize = new Size(1024, 768);
return new(
outerBounds, marginBounds, borderBounds, paddingBounds, contentBounds); // find the size of the tallest device in each row and the widest device in each column.
// this tells us how tall each row needs to be and how wide each column needs to be.
var fullSizeGridCellBounds = new RectangleInfo[gridRowCount, gridColumnCount];
var fullSizeRowHeights = new decimal[gridRowCount];
var fullSizeColumnWidths = new decimal[gridColumnCount];
for (var rowIndex = 0; rowIndex < gridRowCount; rowIndex++)
{
for (var columnIndex = 0; columnIndex < gridColumnCount; columnIndex++)
{
var deviceInfo = deviceGrid[rowIndex, columnIndex].DeviceInfo ?? throw new InvalidOperationException();
var deviceBounds = deviceInfo.GetCombinedDisplayArea();
// if the device bounds are empty, it probably means there aren't any
// screens available for this device so we'll use the minimum size instead
// for the grid cell size
var fullSizeGridCell = deviceBounds.IsEmpty
? deviceBounds.Resize(
width: fullSizeGridCellMinSize.Width,
height: fullSizeGridCellMinSize.Height)
: deviceBounds;
fullSizeGridCellBounds[rowIndex, columnIndex] = fullSizeGridCell;
fullSizeRowHeights[rowIndex] = Math.Max(fullSizeRowHeights[rowIndex], fullSizeGridCell.Height);
fullSizeColumnWidths[columnIndex] = Math.Max(fullSizeColumnWidths[columnIndex], fullSizeGridCell.Width);
}
}
// use the row heights to calculate the absolute coordinates of each grid row
var fullSizeRowCoordinates = new decimal[gridRowCount];
fullSizeRowCoordinates[0] = 0;
for (var rowIndex = 1; rowIndex < gridRowCount; rowIndex++)
{
fullSizeRowCoordinates[rowIndex] = fullSizeRowCoordinates[rowIndex - 1] + fullSizeRowHeights[rowIndex - 1];
}
// use the column widths to calculate the absolute coordinates of each grid column
var fullSizeColumnCoordinates = new decimal[gridColumnCount];
fullSizeColumnCoordinates[0] = 0;
for (var columnIndex = 1; columnIndex < gridColumnCount; columnIndex++)
{
fullSizeColumnCoordinates[columnIndex] = fullSizeColumnCoordinates[columnIndex - 1] + fullSizeColumnWidths[columnIndex - 1];
}
// calculate the full size bounds for the entire grid
var fullSizeGridBounds = new RectangleInfo(
x: 0,
y: 0,
width: fullSizeColumnCoordinates[gridColumnCount - 1] + fullSizeColumnWidths[gridColumnCount - 1],
height: fullSizeRowCoordinates[gridRowCount - 1] + fullSizeRowHeights[gridRowCount - 1]);
// work out the scaling factor that will fit the full size grid into the content bounds
var scaledGridSize = fullSizeGridBounds.Size
.ScaleToFit(contentBounds.Size, out var scalingFactor);
// scale the row and column coordinates by the factor. round coordinates to ensure they
// don't overlap when rendered as pixel dimensions in the image. we'll also add a final
// "fencepost" coordinate to simplify building the end cell of each row and column in the
// scaled grid.
var scaledRowCoordinates = fullSizeRowCoordinates
.Select(fullSizeRowCoordinate => Math.Round(fullSizeRowCoordinate * scalingFactor))
.Concat([scaledGridSize.Height])
.Select(scaledRowCoordinate => scaledRowCoordinate + contentBounds.Top)
.ToArray();
var scaledColumnCoordinates = fullSizeColumnCoordinates
.Select(fullSizeColumnCoordinate => Math.Round(fullSizeColumnCoordinate * scalingFactor))
.Concat([scaledGridSize.Width])
.Select(scaledColumnCoordinate => scaledColumnCoordinate + contentBounds.Left)
.ToArray();
// arrange the device layouts into their scaled grid positions
for (var rowIndex = 0; rowIndex < gridRowCount; rowIndex++)
{
for (var columnIndex = 0; columnIndex < gridColumnCount; columnIndex++)
{
var fullSizeCellSize = fullSizeGridCellBounds[rowIndex, columnIndex].Size;
if ((fullSizeCellSize.Width == 0) || (fullSizeCellSize.Height == 0))
{
// if the full size bounds are zero width or height, it probably means there aren't any
// screens available for this device so we can't scale it to fit inside the grid cell
continue;
}
// work out the scaled coordinates for this grid cell
var scaledGridCellBounds = new RectangleInfo(
x: scaledColumnCoordinates[columnIndex],
y: scaledRowCoordinates[rowIndex],
width: scaledColumnCoordinates[columnIndex + 1] - scaledColumnCoordinates[columnIndex],
height: scaledRowCoordinates[rowIndex + 1] - scaledRowCoordinates[rowIndex]);
// scale and center the device layout to fit inside the grid cell
var deviceLayout = deviceGrid[rowIndex, columnIndex];
deviceLayout.DeviceBounds = BoxBounds.CreateFromOuterBounds(
outerBounds: new RectangleInfo(
size: fullSizeCellSize.ScaleToFit(scaledGridCellBounds.Size))
.Center(scaledGridCellBounds.Midpoint)
.Round()
.Intersect(scaledGridCellBounds.Round()),
boxStyle: deviceLayout.DeviceStyle);
}
}
// verify that we're pixel-perfect on the positions
var scaledDeviceBounds = RectangleInfo.Union(
deviceLayouts
.Select(deviceLayout => deviceLayout.DeviceBounds.OuterBounds));
Debug.Assert(
(scaledDeviceBounds.Width == contentBounds.Width) || (scaledDeviceBounds.Height == contentBounds.Height),
string.Join(
"\r\n",
$"{nameof(ArrangeAndScaleDeviceLayouts)} - scaled device layout does not perfectly fill content bounds:",
$"scaled grid = '{scaledDeviceBounds}'",
$"content bounds = '{contentBounds}'"));
} }
/// <summary> /// <summary>
/// Calculates the bounds of the various areas of a box, given the outer bounds and the box style. /// Arranges the screen layouts inside their respective device cells and
/// This method starts with the outer bounds and works inward, shrinking the outer bounds by the margin, border, and padding sizes to calculate the content bounds of the box. /// scales them to fit inside their parent device layouts.
/// </summary> /// </summary>
/// <param name="outerBounds">The outer bounds of the box.</param> internal static void ArrangeAndScaleScreenLayouts(FormViewModel.Builder formLayout)
/// <param name="boxStyle">The style of the box, which includes the sizes of the margin, border, and padding areas.</param>
/// <returns>A <see cref="BoxBounds"/> object that represents the bounds of the different areas of the box.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="outerBounds"/> or <paramref name="boxStyle"/> is null.</exception>
/// <exception cref="ArgumentException">Thrown when any of the styles in <paramref name="boxStyle"/> is null.</exception>
public static BoxBounds GetBoxBoundsFromOuterBounds(
RectangleInfo outerBounds,
BoxStyle boxStyle)
{ {
ArgumentNullException.ThrowIfNull(outerBounds); var deviceLayouts = formLayout.CanvasLayout?.DeviceLayouts
ArgumentNullException.ThrowIfNull(boxStyle); ?? throw new InvalidOperationException();
if (outerBounds == null || boxStyle.MarginStyle == null || boxStyle.BorderStyle == null || boxStyle.PaddingStyle == null)
{
throw new ArgumentException(null, nameof(boxStyle));
}
var marginBounds = outerBounds; foreach (var deviceLayout in deviceLayouts)
var borderBounds = marginBounds.Shrink(boxStyle.MarginStyle); {
var paddingBounds = borderBounds.Shrink(boxStyle.BorderStyle); var deviceInfo = deviceLayout.DeviceInfo ?? throw new InvalidOperationException();
var contentBounds = paddingBounds.Shrink(boxStyle.PaddingStyle); var screenLayouts = deviceLayout.ScreenLayouts ?? throw new InvalidOperationException();
return new( if (screenLayouts.Count == 0)
outerBounds, marginBounds, borderBounds, paddingBounds, contentBounds); {
// nothing to arrange or scale, and we'll get a divide by zero error
// in ScaleToFitRatio if we don't quit early
continue;
}
// work out the scaling factor that will fit the screen layouts into the content bounds
var fullSizeDisplayArea = deviceInfo.GetCombinedDisplayArea();
var contentBounds = deviceLayout.DeviceBounds.ContentBounds;
var scalingFactor = fullSizeDisplayArea.Size.ScaleToFitRatio(contentBounds.Size);
foreach (var screenLayout in screenLayouts)
{
var screenInfo = screenLayout.ScreenInfo ?? throw new InvalidOperationException();
screenLayout.ScreenBounds = BoxBounds.CreateFromOuterBounds(
outerBounds: screenInfo.DisplayArea
.Offset(-fullSizeDisplayArea.Left, -fullSizeDisplayArea.Top)
.Scale(scalingFactor)
.Offset(contentBounds.Left, contentBounds.Top)
.Round()
.Intersect(contentBounds),
boxStyle: screenLayout.ScreenStyle);
}
// verify that we're pixel-perfect on the positions
var scaledScreenBounds = RectangleInfo.Union(
screenLayouts
.Select(screenLayout => screenLayout.ScreenBounds.OuterBounds));
Debug.Assert(
(scaledScreenBounds.Width == contentBounds.Width) || (scaledScreenBounds.Height == contentBounds.Height),
string.Join(
"\r\n",
$"{nameof(ArrangeAndScaleScreenLayouts)} - scaled screen layouts do not perfectly fill content bounds:",
$"scaled grid = '{scaledScreenBounds}'",
$"content bounds = '{contentBounds}'"));
}
}
internal static void ArrangeAndResizeCanvasLayout(FormViewModel.Builder formLayout)
{
var canvasLayout = formLayout.CanvasLayout ?? throw new InvalidOperationException();
// work out how big the canvas needs to be in order to contain the device layouts
var canvasContentBounds = RectangleInfo.Union(
(canvasLayout.DeviceLayouts ?? throw new InvalidOperationException())
.Select(deviceLayout => deviceLayout.DeviceBounds.OuterBounds));
var canvasOuterBounds = BoxBounds.CreateFromContentBounds(
contentBounds: canvasContentBounds,
boxStyle: formLayout.CanvasLayout.CanvasStyle);
// move the canvas into position
var positionedOuterBounds = canvasOuterBounds.OuterBounds.MoveTo(
(canvasLayout.CanvasBounds ?? throw new InvalidOperationException())
.OuterBounds.Location);
canvasLayout.CanvasBounds = BoxBounds.CreateFromOuterBounds(
outerBounds: positionedOuterBounds,
boxStyle: formLayout.CanvasLayout.CanvasStyle);
}
internal static void ArrangeAndResizeFormLayout(FormViewModel.Builder formLayout, ScreenInfo activatedScreen, PointInfo activatedLocation)
{
var canvasOuterBounds = formLayout.CanvasLayout?.CanvasBounds?.OuterBounds
?? throw new InvalidOperationException();
// resize and center the form on the activated location
formLayout.FormBounds = canvasOuterBounds
.Center(activatedLocation)
.MoveInside(activatedScreen.DisplayArea);
} }
} }

View File

@@ -5,10 +5,12 @@
using System.ComponentModel; using System.ComponentModel;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using MouseJump.Common.Models.Drawing; using MouseJump.Common.Interop;
using MouseJump.Common.NativeMethods; using MouseJump.Models.Drawing;
using static MouseJump.Common.NativeMethods.Core;
using static MouseJump.Common.NativeMethods.User32; using Windows.Win32;
using Windows.Win32.UI.Input.KeyboardAndMouse;
using Windows.Win32.UI.WindowsAndMessaging;
namespace MouseJump.Common.Helpers; namespace MouseJump.Common.Helpers;
@@ -35,19 +37,9 @@ public static class MouseHelper
/// </summary> /// </summary>
public static PointInfo GetCursorPosition() public static PointInfo GetCursorPosition()
{ {
var lpPoint = new LPPOINT(new POINT(0, 0)); var result = PInvoke.GetCursorPos(out var point);
var result = User32.GetCursorPos(lpPoint); ResultHandler.ThrowIfZero(result: result, getLastError: true, memberName: nameof(PInvoke.GetCursorPos));
if (!result) return new(point.X, point.Y);
{
throw new Win32Exception(
Marshal.GetLastWin32Error());
}
var point = lpPoint.ToStructure();
lpPoint.Free();
return new PointInfo(
point.x, point.y);
} }
/// <summary> /// <summary>
@@ -56,43 +48,52 @@ public static class MouseHelper
/// <remarks> /// <remarks>
/// See https://github.com/mikeclayton/FancyMouse/pull/3 /// See https://github.com/mikeclayton/FancyMouse/pull/3
/// </remarks> /// </remarks>
public static void SetCursorPosition(PointInfo location) public static void SetCursorPosition(PointInfo position)
{
MouseHelper.SetCursorPositionInternal(position);
// temporary workaround for issue #1273
MouseHelper.SimulateMouseMovementEvent(position);
}
private static void SetCursorPositionInternal(PointInfo position)
{ {
// set the new cursor position *twice* - the cursor sometimes end up in // set the new cursor position *twice* - the cursor sometimes end up in
// the wrong place if we try to cross the dead space between non-aligned // the wrong place if we try to cross the dead space between non-aligned
// monitors - e.g. when trying to move the cursor from (a) to (b) we can // monitors - e.g. when trying to move the cursor from (a) to (b) through
// *sometimes* - for no clear reason - end up at (c) instead. // the dotted area we can *sometimes* - for no clear reason - end up at
// (c) instead.
// //
// +----------------+ // ..........+----------------+
// |(c) (b) | // ..........|(c) (b) |
// | | // ..........| |
// | | // ..........| |
// | | // ..........| |
// +---------+ | // +---------+ |
// | (a) | | // | (a) | |
// +---------+----------------+ // +---------+----------------+
// //
// setting the position again seems to fix this and moves the // setting the position more than once seems to fix this and moves the
// cursor to the expected location (b) // cursor to the expected location (b)
var target = location.ToPoint(); var targetPosition = position.ToPoint();
for (var i = 0; i < 2; i++) for (var i = 0; i < 2; i++)
{ {
var result = User32.SetCursorPos(target.X, target.Y); // SetCursorPos has been known to return zero (i.e. an error),
if (!result) // with GetLastError also returning zero to indicate success
var result1 = PInvoke.SetCursorPos(targetPosition.X, targetPosition.Y);
if (result1 == 0)
{ {
throw new Win32Exception( var lastError = Marshal.GetLastPInvokeError();
Marshal.GetLastWin32Error()); ResultHandler.HandleResult(result: result1, success: lastError == 0, lastError: lastError, memberName: nameof(PInvoke.SetCursorPos));
} }
var current = MouseHelper.GetCursorPosition(); var result2 = PInvoke.GetCursorPos(out var currentPosition);
if ((current.X == target.X) || (current.Y == target.Y)) ResultHandler.ThrowIfZero(result: result2, getLastError: true, memberName: nameof(PInvoke.GetCursorPos));
if ((currentPosition.X == position.X) && (currentPosition.Y == position.Y))
{ {
break; break;
} }
} }
// temporary workaround for issue #1273
MouseHelper.SimulateMouseMovementEvent(location);
} }
/// <summary> /// <summary>
@@ -104,27 +105,31 @@ public static class MouseHelper
/// </remarks> /// </remarks>
private static void SimulateMouseMovementEvent(PointInfo location) private static void SimulateMouseMovementEvent(PointInfo location)
{ {
var inputs = new User32.INPUT[] var inputs = new INPUT[]
{ {
new( new()
type: INPUT_TYPE.INPUT_MOUSE, {
data: new INPUT.DUMMYUNIONNAME( type = INPUT_TYPE.INPUT_MOUSE,
mi: new MOUSEINPUT( Anonymous = new()
dx: (int)MouseHelper.CalculateAbsoluteCoordinateX(location.X), {
dy: (int)MouseHelper.CalculateAbsoluteCoordinateY(location.Y), mi = new MOUSEINPUT
mouseData: 0, {
dwFlags: MOUSE_EVENT_FLAGS.MOUSEEVENTF_MOVE | MOUSE_EVENT_FLAGS.MOUSEEVENTF_ABSOLUTE, dx = (int)MouseHelper.CalculateAbsoluteCoordinateX(location.X),
time: 0, dy = (int)MouseHelper.CalculateAbsoluteCoordinateY(location.Y),
dwExtraInfo: ULONG_PTR.Null))), mouseData = 0,
dwFlags = MOUSE_EVENT_FLAGS.MOUSEEVENTF_MOVE | MOUSE_EVENT_FLAGS.MOUSEEVENTF_ABSOLUTE,
time = 0,
dwExtraInfo = default,
},
},
},
}; };
var result = User32.SendInput(
(UINT)inputs.Length, var cbSize = Marshal.SizeOf<INPUT>();
new LPINPUT(inputs), var result = PInvoke.SendInput(inputs, cbSize);
INPUT.Size * inputs.Length);
if (result != inputs.Length) if (result != inputs.Length)
{ {
throw new Win32Exception( ResultHandler.HandleFailure(result: result, getLastError: true, memberName: nameof(PInvoke.SendInput));
Marshal.GetLastWin32Error());
} }
} }
@@ -132,13 +137,17 @@ public static class MouseHelper
{ {
// If MOUSEEVENTF_ABSOLUTE value is specified, dx and dy contain normalized absolute coordinates between 0 and 65,535. // If MOUSEEVENTF_ABSOLUTE value is specified, dx and dy contain normalized absolute coordinates between 0 and 65,535.
// see https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-mouseinput // see https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-mouseinput
return (x * 65535) / User32.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CXSCREEN); var result = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CXSCREEN);
ResultHandler.ThrowIfZero(result: result, getLastError: false, memberName: nameof(PInvoke.GetSystemMetrics));
return (x * 65535) / result;
} }
private static decimal CalculateAbsoluteCoordinateY(decimal y) private static decimal CalculateAbsoluteCoordinateY(decimal y)
{ {
// If MOUSEEVENTF_ABSOLUTE value is specified, dx and dy contain normalized absolute coordinates between 0 and 65,535. // If MOUSEEVENTF_ABSOLUTE value is specified, dx and dy contain normalized absolute coordinates between 0 and 65,535.
// see https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-mouseinput // see https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-mouseinput
return (y * 65535) / User32.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CYSCREEN); var result = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CYSCREEN);
ResultHandler.ThrowIfZero(result: result, getLastError: false, memberName: nameof(PInvoke.GetSystemMetrics));
return (y * 65535) / result;
} }
} }

View File

@@ -2,12 +2,16 @@
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System.ComponentModel; using System.Runtime.InteropServices;
using MouseJump.Common.Models.Drawing; using MouseJump.Common.Interop;
using MouseJump.Common.NativeMethods; using MouseJump.Models.Display;
using static MouseJump.Common.NativeMethods.Core; using MouseJump.Models.Drawing;
using static MouseJump.Common.NativeMethods.User32;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.Graphics.Gdi;
using Windows.Win32.UI.WindowsAndMessaging;
namespace MouseJump.Common.Helpers; namespace MouseJump.Common.Helpers;
@@ -20,49 +24,47 @@ public static class ScreenHelper
private static RectangleInfo GetVirtualScreen() private static RectangleInfo GetVirtualScreen()
{ {
return new( return new(
User32.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_XVIRTUALSCREEN), PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_XVIRTUALSCREEN),
User32.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_YVIRTUALSCREEN), PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_YVIRTUALSCREEN),
User32.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CXVIRTUALSCREEN), PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CXVIRTUALSCREEN),
User32.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CYVIRTUALSCREEN)); PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CYVIRTUALSCREEN));
} }
public static IEnumerable<ScreenInfo> GetAllScreens() public static IEnumerable<ScreenInfo> GetAllScreens()
{ {
// enumerate the monitors attached to the system // enumerate the monitors attached to the system
var hMonitors = new List<HMONITOR>(); var hMonitors = new List<HMONITOR>();
var callback = new User32.MONITORENUMPROC( unsafe
(hMonitor, hdcMonitor, lprcMonitor, dwData) =>
{
hMonitors.Add(hMonitor);
return true;
});
var result = User32.EnumDisplayMonitors(HDC.Null, LPCRECT.Null, callback, LPARAM.Null);
if (!result)
{ {
throw new Win32Exception( var callback = new MONITORENUMPROC(
result.Value, (hMonitor, hdcMonitor, lprcMonitor, dwData) =>
$"{nameof(User32.EnumDisplayMonitors)} failed with return code {result.Value}"); {
hMonitors.Add(hMonitor);
return true;
});
var result = PInvoke.EnumDisplayMonitors(HDC.Null, null, callback, (LPARAM)0);
if (result == 0)
{
throw new InvalidOperationException("failed to enumerate monitors");
}
// prevent callback from being collected during the enumeration
GC.KeepAlive(callback);
} }
// get detailed info about each monitor // get detailed info about each monitor
var monitorInfo = new MONITORINFO
{
cbSize = (uint)Marshal.SizeOf<MONITORINFO>(),
};
foreach (var hMonitor in hMonitors) foreach (var hMonitor in hMonitors)
{ {
var monitorInfoPtr = new LPMONITORINFO( var result = PInvoke.GetMonitorInfo(hMonitor, ref monitorInfo);
new MONITORINFO((DWORD)MONITORINFO.Size, RECT.Empty, RECT.Empty, 0)); ResultHandler.ThrowIfZero(result: result, getLastError: true, memberName: nameof(PInvoke.GetMonitorInfo));
result = User32.GetMonitorInfoW(hMonitor, monitorInfoPtr);
if (!result)
{
throw new Win32Exception(
result.Value,
$"{nameof(User32.GetMonitorInfoW)} failed with return code {result.Value}");
}
var monitorInfo = monitorInfoPtr.ToStructure();
monitorInfoPtr.Free();
yield return new ScreenInfo( yield return new ScreenInfo(
handle: hMonitor, handle: hMonitor,
primary: monitorInfo.dwFlags.HasFlag(User32.MONITOR_INFO_FLAGS.MONITORINFOF_PRIMARY), primary: (monitorInfo.dwFlags & PInvoke.MONITORINFOF_PRIMARY) != 0,
displayArea: new RectangleInfo( displayArea: new RectangleInfo(
monitorInfo.rcMonitor.left, monitorInfo.rcMonitor.left,
monitorInfo.rcMonitor.top, monitorInfo.rcMonitor.top,
@@ -81,9 +83,9 @@ public static class ScreenHelper
PointInfo pt) PointInfo pt)
{ {
// get the monitor handle from the point // get the monitor handle from the point
var hMonitor = User32.MonitorFromPoint( var hMonitor = PInvoke.MonitorFromPoint(
new((int)pt.X, (int)pt.Y), new((int)pt.X, (int)pt.Y),
User32.MONITOR_FROM_FLAGS.MONITOR_DEFAULTTONEAREST); MONITOR_FROM_FLAGS.MONITOR_DEFAULTTONEAREST);
if (hMonitor.IsNull) if (hMonitor.IsNull)
{ {
throw new InvalidOperationException($"no monitor found for point {pt}"); throw new InvalidOperationException($"no monitor found for point {pt}");

View File

@@ -2,8 +2,10 @@
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using MouseJump.Common.Models.Drawing; using System.Drawing;
using MouseJump.Common.Models.Styles;
using MouseJump.Models.Drawing;
using MouseJump.Models.Styles;
namespace MouseJump.Common.Helpers; namespace MouseJump.Common.Helpers;
@@ -46,7 +48,13 @@ public static class StyleHelper
color1: Color.MidnightBlue, color1: Color.MidnightBlue,
color2: Color.MidnightBlue color2: Color.MidnightBlue
) )
) ),
extraColors: new[]
{
Color.Red,
Color.Blue,
Color.Green,
}
); );
/// <summary> /// <summary>
@@ -86,7 +94,13 @@ public static class StyleHelper
color1: Color.MidnightBlue, color1: Color.MidnightBlue,
color2: Color.MidnightBlue color2: Color.MidnightBlue
) )
) ),
extraColors: new[]
{
Color.Red,
Color.Blue,
Color.Green,
}
); );
public static PreviewStyle WithCanvasSize(this PreviewStyle previewStyle, SizeInfo canvasSize) public static PreviewStyle WithCanvasSize(this PreviewStyle previewStyle, SizeInfo canvasSize)
@@ -96,6 +110,7 @@ public static class StyleHelper
return new PreviewStyle( return new PreviewStyle(
canvasSize: canvasSize, canvasSize: canvasSize,
canvasStyle: previewStyle.CanvasStyle, canvasStyle: previewStyle.CanvasStyle,
screenStyle: previewStyle.ScreenStyle); screenStyle: previewStyle.ScreenStyle,
extraColors: previewStyle.ExtraColors);
} }
} }

View File

@@ -0,0 +1,111 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Diagnostics;
using System.Runtime.InteropServices;
using MouseJump.Common.Interop;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.Graphics.Gdi;
using Windows.Win32.UI.WindowsAndMessaging;
namespace MouseJump.Common.Helpers;
public static class Win32Helper
{
/// <summary>
/// Creates a new "message only" window.
/// </summary>
/// <remarks>
/// A message-only window is a window that is not visible and does not have a user interface.
/// It exists solely to receive and process messages.
/// These windows are used for background tasks that need to handle messages without displaying any graphical elements to the user.
/// </remarks>
public static Win32Window CreateMessageOnlyWindow(string className, string windowName, Win32WindowProc.WindowProcDelegate windowProcDelegate)
{
ArgumentNullException.ThrowIfNullOrEmpty(className);
ArgumentNullException.ThrowIfNullOrEmpty(windowName);
ArgumentNullException.ThrowIfNull(windowProcDelegate);
var windowClass = Win32Helper.RegisterWindowClass(className, windowProcDelegate);
var window = Win32Helper.CreateWindow(windowClass, windowName);
return window;
}
private static Win32WindowClass RegisterWindowClass(string className, Win32WindowProc.WindowProcDelegate windowProcDelegate)
{
ArgumentNullException.ThrowIfNullOrEmpty(className);
ArgumentNullException.ThrowIfNull(windowProcDelegate);
using var hModule = PInvoke.GetModuleHandle(null);
var hInstance = (HINSTANCE)hModule.DangerousGetHandle();
// register the window class
// see https://stackoverflow.com/a/30992796/3156906
var windowProc = new Win32WindowProc(windowProcDelegate);
var classAtom = default(ushort);
unsafe
{
fixed (char* classNamePtr = className)
{
var wndClass = new WNDCLASSEXW
{
cbSize = (uint)Marshal.SizeOf<WNDCLASSEXW>(),
style = 0,
lpfnWndProc = windowProc.WndProcDelegate,
cbClsExtra = 0,
cbWndExtra = 0,
hInstance = hInstance,
hIcon = HICON.Null,
hCursor = HCURSOR.Null,
hbrBackground = HBRUSH.Null,
lpszMenuName = default,
lpszClassName = new PCWSTR(classNamePtr),
hIconSm = HICON.Null,
};
classAtom = PInvoke.RegisterClassEx(wndClass);
}
}
ResultHandler.ThrowIfZero(result: classAtom, getLastError: true, memberName: nameof(PInvoke.RegisterClassEx));
var windowClass = new Win32WindowClass(classAtom, className, windowProc);
return windowClass;
}
private static Win32Window CreateWindow(Win32WindowClass windowClass, string windowName)
{
// create the window
// see https://learn.microsoft.com/en-us/windows/win32/winmsg/window-features#message-only-windows
// https://devblogs.microsoft.com/oldnewthing/20171218-00/?p=97595
// https://stackoverflow.com/a/30992796/3156906
var hWnd = default(HWND);
using var hModule = PInvoke.GetModuleHandle(null);
unsafe
{
hWnd = PInvoke.CreateWindowEx(
dwExStyle: 0,
lpClassName: windowClass.ClassName,
lpWindowName: windowName,
dwStyle: 0,
X: 0,
Y: 0,
nWidth: 300,
nHeight: 400,
hWndParent: HWND.HWND_MESSAGE, // message-only window
hMenu: null,
hInstance: hModule,
lpParam: null);
}
ResultHandler.ThrowIfZero(result: hWnd, getLastError: true, memberName: nameof(PInvoke.CreateWindowEx));
var window = new Win32Window(windowClass, windowName, hWnd);
return window;
}
}

View File

@@ -3,10 +3,14 @@
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System.Diagnostics; using System.Diagnostics;
using System.Drawing;
using MouseJump.Common.Models.Drawing; using MouseJump.Common.Interop;
using MouseJump.Common.NativeMethods; using MouseJump.Models.Drawing;
using static MouseJump.Common.NativeMethods.Core;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.Graphics.Gdi;
namespace MouseJump.Common.Imaging; namespace MouseJump.Common.Imaging;
@@ -28,12 +32,12 @@ public sealed class DesktopImageRegionCopyService : IImageRegionCopyService
var stopwatch = Stopwatch.StartNew(); var stopwatch = Stopwatch.StartNew();
var (desktopHwnd, desktopHdc) = DesktopImageRegionCopyService.GetDesktopDeviceContext(); var (desktopHwnd, desktopHdc) = DesktopImageRegionCopyService.GetDesktopDeviceContext();
var previewHdc = DesktopImageRegionCopyService.GetGraphicsDeviceContext( var previewHdc = DesktopImageRegionCopyService.GetGraphicsDeviceContext(
targetGraphics, Gdi32.STRETCH_BLT_MODE.STRETCH_HALFTONE); targetGraphics, STRETCH_BLT_MODE.STRETCH_HALFTONE);
stopwatch.Stop(); stopwatch.Stop();
var source = sourceBounds.ToRectangle(); var source = sourceBounds.ToRectangle();
var target = targetBounds.ToRectangle(); var target = targetBounds.ToRectangle();
var result = Gdi32.StretchBlt( var result = PInvoke.StretchBlt(
previewHdc, previewHdc,
target.X, target.X,
target.Y, target.Y,
@@ -44,15 +48,11 @@ public sealed class DesktopImageRegionCopyService : IImageRegionCopyService
source.Y, source.Y,
source.Width, source.Width,
source.Height, source.Height,
Gdi32.ROP_CODE.SRCCOPY); ROP_CODE.SRCCOPY);
if (!result) ResultHandler.ThrowIfZero(result: result, getLastError: false, memberName: nameof(PInvoke.StretchBlt));
{
throw new InvalidOperationException(
$"{nameof(Gdi32.StretchBlt)} returned {result.Value}");
}
// we need to release the graphics device context handle before anything // we need to release the graphics device context handle before anything
// else tries to use the Graphics object otherwise it'll give an error // else tries to use the Graphics object - otherwise it'll give an error
// from GDI saying "Object is currently in use elsewhere" // from GDI saying "Object is currently in use elsewhere"
DesktopImageRegionCopyService.FreeGraphicsDeviceContext(targetGraphics, ref previewHdc); DesktopImageRegionCopyService.FreeGraphicsDeviceContext(targetGraphics, ref previewHdc);
@@ -61,14 +61,9 @@ public sealed class DesktopImageRegionCopyService : IImageRegionCopyService
private static (HWND DesktopHwnd, HDC DesktopHdc) GetDesktopDeviceContext() private static (HWND DesktopHwnd, HDC DesktopHdc) GetDesktopDeviceContext()
{ {
var desktopHwnd = User32.GetDesktopWindow(); var desktopHwnd = PInvoke.GetDesktopWindow();
var desktopHdc = User32.GetWindowDC(desktopHwnd); var desktopHdc = PInvoke.GetWindowDC(desktopHwnd);
if (desktopHdc.IsNull) ResultHandler.HandleResult(result: desktopHdc, success: !desktopHdc.IsNull, getLastError: false, memberName: nameof(PInvoke.GetWindowDC));
{
throw new InvalidOperationException(
$"{nameof(User32.GetWindowDC)} returned null");
}
return (desktopHwnd, desktopHdc); return (desktopHwnd, desktopHdc);
} }
@@ -76,12 +71,8 @@ public sealed class DesktopImageRegionCopyService : IImageRegionCopyService
{ {
if (!desktopHwnd.IsNull && !desktopHdc.IsNull) if (!desktopHwnd.IsNull && !desktopHdc.IsNull)
{ {
var result = User32.ReleaseDC(desktopHwnd, desktopHdc); var result = PInvoke.ReleaseDC(desktopHwnd, desktopHdc);
if (result == 0) ResultHandler.ThrowIfZero(result: result, getLastError: false, memberName: nameof(PInvoke.ReleaseDC));
{
throw new InvalidOperationException(
$"{nameof(User32.ReleaseDC)} returned {result}");
}
} }
desktopHwnd = HWND.Null; desktopHwnd = HWND.Null;
@@ -92,17 +83,11 @@ public sealed class DesktopImageRegionCopyService : IImageRegionCopyService
/// Checks if the target device context handle exists, and creates a new one from the /// Checks if the target device context handle exists, and creates a new one from the
/// specified Graphics object if not. /// specified Graphics object if not.
/// </summary> /// </summary>
private static HDC GetGraphicsDeviceContext(Graphics graphics, Gdi32.STRETCH_BLT_MODE mode) private static HDC GetGraphicsDeviceContext(Graphics graphics, STRETCH_BLT_MODE mode)
{ {
var graphicsHdc = (HDC)graphics.GetHdc(); var graphicsHdc = (HDC)graphics.GetHdc();
var result = PInvoke.SetStretchBltMode(graphicsHdc, mode);
var result = Gdi32.SetStretchBltMode(graphicsHdc, mode); ResultHandler.ThrowIfZero(result: result, getLastError: false, memberName: nameof(PInvoke.SetStretchBltMode));
if (result == 0)
{
throw new InvalidOperationException(
$"{nameof(Gdi32.SetStretchBltMode)} returned {result}");
}
return graphicsHdc; return graphicsHdc;
} }
@@ -116,7 +101,7 @@ public sealed class DesktopImageRegionCopyService : IImageRegionCopyService
return; return;
} }
graphics.ReleaseHdc(graphicsHdc.Value); graphics.ReleaseHdc(graphicsHdc);
graphicsHdc = HDC.Null; graphicsHdc = HDC.Null;
} }
} }

View File

@@ -2,7 +2,9 @@
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using MouseJump.Common.Models.Drawing; using System.Drawing;
using MouseJump.Models.Drawing;
namespace MouseJump.Common.Imaging; namespace MouseJump.Common.Imaging;
@@ -10,7 +12,7 @@ public interface IImageRegionCopyService
{ {
/// <summary> /// <summary>
/// Copies the source region from the provider's source image (e.g. the interactive desktop, /// Copies the source region from the provider's source image (e.g. the interactive desktop,
/// a static image, etc.) to the target region on the specified Graphics object. /// a static image, etc) to the target region on the specified Graphics object.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Implementations of this interface are used to capture regions of the interactive desktop /// Implementations of this interface are used to capture regions of the interactive desktop

View File

@@ -0,0 +1,47 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Drawing;
using System.Drawing.Drawing2D;
using MouseJump.Models.Drawing;
namespace MouseJump.Common.Imaging;
/// <summary>
/// Implements an IImageRegionCopyService that paints a solid block of the specified color.
/// This is used for testing the DrawingHelper rather than as part of the main application.
/// </summary>
public sealed class SolidColorRegionCopyService : IImageRegionCopyService
{
public SolidColorRegionCopyService(Color color)
{
this.Color = color;
}
private Color Color
{
get;
}
/// <summary>
/// Draws a region in a solid color.
/// </summary>
public void CopyImageRegion(
Graphics targetGraphics,
RectangleInfo sourceBounds,
RectangleInfo targetBounds)
{
using var brush = new SolidBrush(this.Color);
// prevent the background bleeding through into screen images
// (see https://github.com/mikeclayton/FancyMouse/issues/44)
targetGraphics.PixelOffsetMode = PixelOffsetMode.Half;
targetGraphics.InterpolationMode = InterpolationMode.NearestNeighbor;
targetGraphics.FillRectangle(
brush: brush,
rect: targetBounds.ToRectangle());
}
}

View File

@@ -2,9 +2,10 @@
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System.Drawing;
using System.Drawing.Drawing2D; using System.Drawing.Drawing2D;
using MouseJump.Common.Models.Drawing; using MouseJump.Models.Drawing;
namespace MouseJump.Common.Imaging; namespace MouseJump.Common.Imaging;

View File

@@ -0,0 +1,121 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace MouseJump.Common.Interop;
public static partial class ResultHandler
{
/*
helpers for handling results from Win32 API calls that return an int value,
throwing exceptions when errors occur, and reading the last error code when necessary.
*/
public static void ThrowIfZero(
int result,
bool getLastError = false,
[CallerMemberName] string memberName = "")
{
ResultHandler.HandleResult(
result,
result != 0,
getLastError,
memberName);
}
public static void ThrowIfNotZero(
int result,
bool getLastError = false,
[CallerMemberName] string memberName = "")
{
ResultHandler.HandleResult(
result,
result == 0,
getLastError,
memberName);
}
public static void HandleResult(
int result,
bool success,
bool getLastError = false,
[CallerMemberName] string memberName = "")
{
if (success)
{
return;
}
ResultHandler.HandleFailure(result, getLastError, memberName);
}
public static void HandleResult(
int result,
bool success,
Func<int>? getLastError,
[CallerMemberName] string memberName = "")
{
if (success)
{
return;
}
var lastError = getLastError?.Invoke();
ResultHandler.HandleFailure(result, lastError, memberName);
}
public static void HandleResult(
int result,
bool success,
int? lastError,
[CallerMemberName] string memberName = "")
{
if (success)
{
return;
}
ResultHandler.HandleFailure(result, lastError, memberName);
}
public static void HandleFailure(
int result,
bool getLastError = false,
[CallerMemberName] string memberName = "")
{
var lastError = getLastError ? (int?)Marshal.GetLastPInvokeError() : null;
ResultHandler.HandleFailure(result, lastError, memberName);
}
public static void HandleFailure(
int result,
Func<int>? getLastError,
[CallerMemberName] string memberName = "")
{
var lastError = getLastError?.Invoke();
ResultHandler.HandleFailure(result, lastError, memberName);
}
public static void HandleFailure(
int result,
int? lastError,
[CallerMemberName] string memberName = "")
{
var lines = new List<string>
{
$"{memberName} failed with result {result}.",
};
if (lastError is not null)
{
lines.Add($"last error was '{lastError}'");
}
var message = string.Join(Environment.NewLine, lines);
throw new Win32Exception(message);
}
}

View File

@@ -0,0 +1,121 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace MouseJump.Common.Interop;
public static partial class ResultHandler
{
/*
helpers for handling results from Win32 API calls that return an nint value,
throwing exceptions when errors occur, and reading the last error code when necessary.
*/
public static void ThrowIfZero(
nint result,
bool getLastError = false,
[CallerMemberName] string memberName = "")
{
ResultHandler.HandleResult(
result,
result.ToInt64() != 0,
getLastError,
memberName);
}
public static void ThrowIfNotZero(
nint result,
bool getLastError = false,
[CallerMemberName] string memberName = "")
{
ResultHandler.HandleResult(
result,
result.ToInt64() == 0,
getLastError,
memberName);
}
public static void HandleResult(
nint result,
bool success,
bool getLastError = false,
[CallerMemberName] string memberName = "")
{
if (success)
{
return;
}
ResultHandler.HandleFailure(result, getLastError, memberName);
}
public static void HandleResult(
nint result,
bool success,
Func<int>? getLastError,
[CallerMemberName] string memberName = "")
{
if (success)
{
return;
}
var lastError = getLastError?.Invoke();
ResultHandler.HandleFailure(result, lastError, memberName);
}
public static void HandleResult(
nint result,
bool success,
int? lastError,
[CallerMemberName] string memberName = "")
{
if (success)
{
return;
}
ResultHandler.HandleFailure(result, lastError, memberName);
}
public static void HandleFailure(
nint result,
bool getLastError = false,
[CallerMemberName] string memberName = "")
{
var lastError = getLastError ? (int?)Marshal.GetLastPInvokeError() : null;
ResultHandler.HandleFailure(result, lastError, memberName);
}
public static void HandleFailure(
nint result,
Func<int>? getLastError,
[CallerMemberName] string memberName = "")
{
var lastError = getLastError?.Invoke();
ResultHandler.HandleFailure(result, lastError, memberName);
}
public static void HandleFailure(
nint result,
int? lastError,
[CallerMemberName] string memberName = "")
{
var lines = new List<string>
{
$"{memberName} failed with result {result}.",
};
if (lastError is not null)
{
lines.Add($"last error was '{lastError}'");
}
var message = string.Join(Environment.NewLine, lines);
throw new Win32Exception(message);
}
}

View File

@@ -0,0 +1,54 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace MouseJump.Common.Interop;
public static partial class ResultHandler
{
/*
helpers for handling results from Win32 API calls that return a uint value,
throwing exceptions when errors occur, and reading the last error code when necessary.
*/
public static void HandleFailure(
uint result,
bool getLastError = false,
[CallerMemberName] string memberName = "")
{
var lastError = getLastError ? (int?)Marshal.GetLastPInvokeError() : null;
ResultHandler.HandleFailure(result, lastError, memberName);
}
public static void HandleFailure(
uint result,
Func<int>? getLastError,
[CallerMemberName] string memberName = "")
{
var lastError = getLastError?.Invoke();
ResultHandler.HandleFailure(result, lastError, memberName);
}
public static void HandleFailure(
uint result,
int? lastError,
[CallerMemberName] string memberName = "")
{
var lines = new List<string>
{
$"{memberName} failed with result {result}.",
};
if (lastError is not null)
{
lines.Add($"last error was '{lastError}'");
}
var message = string.Join(Environment.NewLine, lines);
throw new Win32Exception(message);
}
}

View File

@@ -0,0 +1,42 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Runtime.InteropServices;
namespace MouseJump.Common.Interop;
public sealed class Win32SafeHandle : SafeHandle
{
public Win32SafeHandle(nint handle)
: base(IntPtr.Zero, false)
{
this.SetHandle(handle);
}
public Win32SafeHandle(nint handle, bool ownsHandle = false)
: base(IntPtr.Zero, ownsHandle)
{
this.SetHandle(handle);
}
public Win32SafeHandle(nint handle, bool ownsHandle, Func<IntPtr, bool> release)
: base(IntPtr.Zero, ownsHandle)
{
this.ReleaseDelegate = release ?? throw new ArgumentNullException(nameof(release));
this.SetHandle(handle);
}
private Func<IntPtr, bool>? ReleaseDelegate
{
get;
}
public override bool IsInvalid
=> handle == nint.Zero;
protected override bool ReleaseHandle()
{
return this.ReleaseDelegate?.Invoke(this.handle) ?? true;
}
}

View File

@@ -0,0 +1,32 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Windows.Win32.Foundation;
namespace MouseJump.Common.Interop;
public sealed class Win32Window
{
internal Win32Window(Win32WindowClass windowClass, string windowName, HWND hwnd)
{
this.WindowClass = windowClass ?? throw new ArgumentNullException(nameof(windowClass));
this.WindowName = windowName ?? throw new ArgumentNullException(nameof(windowName));
this.Hwnd = hwnd;
}
public Win32WindowClass WindowClass
{
get;
}
public string WindowName
{
get;
}
public nint Hwnd
{
get;
}
}

View File

@@ -0,0 +1,38 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Windows.Win32.Foundation;
using Windows.Win32.UI.WindowsAndMessaging;
namespace MouseJump.Common.Interop;
/// <summary>
/// Wrapper around a Windows window class that can be passed to
/// external assemblies to manage WNDPROC delegate lifetime without
/// exposing internals.
/// </summary>
public sealed class Win32WindowClass
{
internal Win32WindowClass(ushort classAtom, string className, Win32WindowProc windowProc)
{
this.ClassAtom = classAtom;
this.ClassName = className ?? throw new ArgumentNullException(nameof(className));
this.WindowProc = windowProc ?? throw new ArgumentNullException(nameof(windowProc));
}
internal ushort ClassAtom
{
get;
}
public string ClassName
{
get;
}
internal Win32WindowProc WindowProc
{
get;
}
}

View File

@@ -0,0 +1,71 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Windows.Win32.Foundation;
using Windows.Win32.UI.WindowsAndMessaging;
namespace MouseJump.Common.Interop;
/// <summary>
/// Wrapper around a WNDPROC delegate that can be handed to
/// external assemblies to manage native WNDPROC delegate lifetime
/// without exposing internals.
/// </summary>
public sealed class Win32WindowProc
{
/// <summary>
/// Public definition of WNDPROC using CLR types instead of CsWin32 generated types.
/// </summary>
public delegate nint WindowProcDelegate(nint hWnd, uint msg, nuint wParam, nint lParam);
internal Win32WindowProc(WindowProcDelegate windowProc)
{
this.WindowProcInternal = windowProc ?? throw new ArgumentNullException(nameof(windowProc));
// tl;dr...
//
// * Native code stores only the unmanaged function pointer.
// * Managed code must keep the delegate alive for native code to call.
//
// long version...
//
// A *method group* (e.g. WndProcInternal) is *not* a delegate (WNDPROC).
// When a method group is passed to anything that receives a delegate, the
// CLR creates a new temporary delegate instance based on the method group.
//
// Windows api methods such as RegisterClassEx don't store a reference to
// the managed delegate object - they only store an unmanaged function
// pointer. Once the delegate instance becomes unreferenced on the managed
// side the GC is allowed to collect it, even though the Win32 api continues
// to store the unmanaged pointer to it. If Win32 subsequently calls the
// (now invalid) unmanaged function pointer it can cause access violations
// or other runtime crashes.
//
// To prevent this, we'll cache a strongly-typed reference to the managed
// *delegate* instance in WndProcDelegate. Keeping this delegate reference
// alive ensures the underlying unmanaged function pointer remains valid
// for the entire lifetime of this Window instance.
this.WndProcDelegate = this.WndProcInternal;
}
internal ushort ClassAtom
{
get;
}
private WindowProcDelegate WindowProcInternal
{
get;
}
internal WNDPROC WndProcDelegate
{
get;
}
private LRESULT WndProcInternal(HWND hWnd, uint msg, WPARAM wParam, LPARAM lParam)
{
return (LRESULT)this.WindowProcInternal(hWnd, msg, wParam, lParam);
}
}

View File

@@ -1,73 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace MouseJump.Common.Models.Drawing;
public sealed class BoxBounds
{
/*
see https://www.w3schools.com/css/css_boxmodel.asp
+--------------[bounds]---------------+
|▒▒▒▒▒▒▒▒▒▒▒▒▒▒[margin]▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒|
|▒▒▓▓▓▓▓▓▓▓▓▓▓▓[border]▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒|
|▒▒▓▓░░░░░░░░░░[padding]░░░░░░░░░░▓▓▒▒|
|▒▒▓▓░░ ░░▓▓▒▒|
|▒▒▓▓░░ ░░▓▓▒▒|
|▒▒▓▓░░ [content] ░░▓▓▒▒|
|▒▒▓▓░░ ░░▓▓▒▒|
|▒▒▓▓░░ ░░▓▓▒▒|
|▒▒▓▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓▓▒▒|
|▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒|
|▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒|
+-------------------------------------+
*/
public BoxBounds(
RectangleInfo outerBounds,
RectangleInfo marginBounds,
RectangleInfo borderBounds,
RectangleInfo paddingBounds,
RectangleInfo contentBounds)
{
this.OuterBounds = outerBounds ?? throw new ArgumentNullException(nameof(outerBounds));
this.MarginBounds = marginBounds ?? throw new ArgumentNullException(nameof(marginBounds));
this.BorderBounds = borderBounds ?? throw new ArgumentNullException(nameof(borderBounds));
this.PaddingBounds = paddingBounds ?? throw new ArgumentNullException(nameof(paddingBounds));
this.ContentBounds = contentBounds ?? throw new ArgumentNullException(nameof(contentBounds));
}
/// <summary>
/// Gets the outer bounds of this layout box.
/// </summary>
public RectangleInfo OuterBounds
{
get;
}
public RectangleInfo MarginBounds
{
get;
}
public RectangleInfo BorderBounds
{
get;
}
public RectangleInfo PaddingBounds
{
get;
}
/// <summary>
/// Gets the bounds of the content area for this layout box.
/// </summary>
public RectangleInfo ContentBounds
{
get;
}
}

View File

@@ -1,131 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.ObjectModel;
using MouseJump.Common.Models.Drawing;
using MouseJump.Common.Models.Styles;
namespace MouseJump.Common.Models.Layout;
public sealed class PreviewLayout
{
public sealed class Builder
{
public Builder()
{
this.Screens = new();
this.ScreenshotBounds = new();
}
public PreviewStyle? PreviewStyle
{
get;
set;
}
public RectangleInfo? VirtualScreen
{
get;
set;
}
public List<RectangleInfo> Screens
{
get;
set;
}
public int ActivatedScreenIndex
{
get;
set;
}
public RectangleInfo? FormBounds
{
get;
set;
}
public BoxBounds? PreviewBounds
{
get;
set;
}
public List<BoxBounds> ScreenshotBounds
{
get;
set;
}
public PreviewLayout Build()
{
return new PreviewLayout(
previewStyle: this.PreviewStyle ?? throw new InvalidOperationException($"{nameof(this.PreviewStyle)} must be initialized before calling {nameof(this.Build)}."),
virtualScreen: this.VirtualScreen ?? throw new InvalidOperationException($"{nameof(this.VirtualScreen)} must be initialized before calling {nameof(this.Build)}."),
screens: this.Screens ?? throw new InvalidOperationException($"{nameof(this.Screens)} must be initialized before calling {nameof(this.Build)}."),
activatedScreenIndex: this.ActivatedScreenIndex,
formBounds: this.FormBounds ?? throw new InvalidOperationException($"{nameof(this.FormBounds)} must be initialized before calling {nameof(this.Build)}."),
previewBounds: this.PreviewBounds ?? throw new InvalidOperationException($"{nameof(this.PreviewBounds)} must be initialized before calling {nameof(this.Build)}."),
screenshotBounds: this.ScreenshotBounds ?? throw new InvalidOperationException($"{nameof(this.ScreenshotBounds)} must be initialized before calling {nameof(this.Build)}."));
}
}
public PreviewLayout(
PreviewStyle previewStyle,
RectangleInfo virtualScreen,
List<RectangleInfo> screens,
int activatedScreenIndex,
RectangleInfo formBounds,
BoxBounds previewBounds,
List<BoxBounds> screenshotBounds)
{
this.PreviewStyle = previewStyle ?? throw new ArgumentNullException(nameof(previewStyle));
this.VirtualScreen = virtualScreen ?? throw new ArgumentNullException(nameof(virtualScreen));
this.Screens = (screens ?? throw new ArgumentNullException(nameof(screens)))
.ToList().AsReadOnly();
this.ActivatedScreenIndex = activatedScreenIndex;
this.FormBounds = formBounds ?? throw new ArgumentNullException(nameof(formBounds));
this.PreviewBounds = previewBounds ?? throw new ArgumentNullException(nameof(previewBounds));
this.ScreenshotBounds = (screenshotBounds ?? throw new ArgumentNullException(nameof(screenshotBounds)))
.ToList().AsReadOnly();
}
public PreviewStyle PreviewStyle
{
get;
}
public RectangleInfo VirtualScreen
{
get;
}
public ReadOnlyCollection<RectangleInfo> Screens
{
get;
}
public int ActivatedScreenIndex
{
get;
}
public RectangleInfo FormBounds
{
get;
}
public BoxBounds PreviewBounds
{
get;
}
public ReadOnlyCollection<BoxBounds> ScreenshotBounds
{
get;
}
}

View File

@@ -1,24 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<!-- Look at Directory.Build.props in root for common stuff as well --> <!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="$(RepoRoot)src\Common.Dotnet.CsWinRT.props" /> <Import Project="$(RepoRoot)src\Common.Dotnet.CsWinRT.props" />
<PropertyGroup> <PropertyGroup>
<ProjectGuid>{923DF87C-CA99-4D1C-B1D2-959174E95BFA}</ProjectGuid>
<AssemblyName>PowerToys.MouseJump.Common</AssemblyName>
<AssemblyTitle>PowerToys.MouseJump.Common</AssemblyTitle>
<AssemblyDescription>PowerToys MouseJump.Common</AssemblyDescription>
<OutputType>Library</OutputType>
<OutputPath>$(RepoRoot)$(Platform)\$(Configuration)\WinUI3Apps</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<ProjectGuid>{923DF87C-CA99-4D1C-B1D2-959174E95BFA}</ProjectGuid> <ApplicationHighDpiMode>PerMonitorV2</ApplicationHighDpiMode>
<AssemblyName>PowerToys.MouseJump.Common</AssemblyName> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<AssemblyTitle>PowerToys.MouseJump.Common</AssemblyTitle> <Nullable>enable</Nullable>
<AssemblyDescription>PowerToys MouseJump.Common</AssemblyDescription> <ImplicitUsings>enable</ImplicitUsings>
<OutputType>Library</OutputType> </PropertyGroup>
<OutputPath>$(RepoRoot)$(Platform)\$(Configuration)</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<ApplicationHighDpiMode>PerMonitorV2</ApplicationHighDpiMode> <ItemGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <ProjectReference Include="..\MouseJump.Models\MouseJump.Models.csproj" />
<Nullable>enable</Nullable> </ItemGroup>
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Drawing.Common" />
<PackageReference Include="Microsoft.Windows.CsWin32">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project> </Project>

View File

@@ -0,0 +1,69 @@
HBRUSH
HCURSOR
HICON
HINSTANCE
WM_ACTIVATE
WM_CREATE
WM_ENTERIDLE
WM_ENTERMENULOOP
WM_EXITMENULOOP
WM_KILLFOCUS
WM_GETMINMAXINFO
WM_IME_SETCONTEXT
WM_IME_NOTIFY
WM_INITMENU
WM_INITMENUPOPUP
WM_LBUTTONDOWN
WM_MENUSELECT
WM_NCACTIVATE
WM_NCCALCSIZE
WM_NCCREATE
WM_RBUTTONDOWN
WM_SETCURSOR
WM_SETFOCUS
WM_UNINITMENUPOPUP
WM_USER
WM_WINDOWPOSCHANGED
WM_WINDOWPOSCHANGING
WM_COMMAND
CreateWindowExW
DefWindowProc
GetWindowLong
RegisterClassEx
SetForegroundWindow
WNDPROC
MONITORINFOF_PRIMARY
ScreenToClient
EnumDisplayMonitors
GetMonitorInfo
GetSystemMetrics
MonitorFromPoint
SendInput
GetCursorPos
SetCursorPos
AppendMenu
CreatePopupMenu
SetMenuInfo
TrackPopupMenuEx
TRACK_POPUP_MENU_FLAGS
SetStretchBltMode
StretchBlt
GetDesktopWindow
GetWindowDC
ReleaseDC
Shell_NotifyIconW
GetAwarenessFromDpiAwarenessContext
GetThreadDpiAwarenessContext
HWND_MESSAGE
GetModuleHandleW

View File

@@ -1,54 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
namespace MouseJump.Common.NativeMethods;
internal static partial class Core
{
/// <summary>
/// The CRECT structure defines a rectangle by the coordinates of its upper-left and lower-right corners.
/// </summary>
/// <remarks>
/// See https://learn.microsoft.com/en-us/windows/win32/api/windef/ns-windef-rect
/// </remarks>
[SuppressMessage("Naming Rules", "SA1307:AccessibleFieldsMustBeginWithUpperCaseLetter", Justification = "Name and value taken from Win32Api")]
internal readonly struct CRECT
{
public static readonly CRECT Empty = new(0, 0, 0, 0);
public readonly LONG left;
public readonly LONG top;
public readonly LONG right;
public readonly LONG bottom;
public CRECT(
int left, int top, int right, int bottom)
{
this.left = left;
this.top = top;
this.right = right;
this.bottom = bottom;
}
public CRECT(
LONG left, LONG top, LONG right, LONG bottom)
{
this.left = left;
this.top = top;
this.right = right;
this.bottom = bottom;
}
public static int Size =>
Marshal.SizeOf(typeof(CRECT));
public override string ToString()
{
return $"left={this.left},top={this.top},right={this.right},bottom={this.bottom}";
}
}
}

View File

@@ -1,39 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace MouseJump.Common.NativeMethods;
internal static partial class Core
{
/// <summary>
/// A handle to a device context (DC).
/// This type is declared in WinDef.h as follows:
/// typedef HANDLE HDC;
/// </summary>
/// <remarks>
/// See https://learn.microsoft.com/en-us/windows/win32/winprog/windows-data-types
/// </remarks>
internal readonly struct HDC
{
public static readonly HDC Null = new(IntPtr.Zero);
public readonly IntPtr Value;
public HDC(IntPtr value)
{
this.Value = value;
}
public bool IsNull => this.Value == HDC.Null.Value;
public static implicit operator IntPtr(HDC value) => value.Value;
public static explicit operator HDC(IntPtr value) => new(value);
public override string ToString()
{
return $"{this.GetType().Name}({this.Value})";
}
}
}

View File

@@ -1,47 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace MouseJump.Common.NativeMethods;
internal static partial class Core
{
/// <summary>
/// A handle to a display monitor.
/// This type is declared in WinDef.h as follows:
/// if(WINVER >= 0x0500) typedef HANDLE HMONITOR;
/// </summary>
/// <remarks>
/// See https://learn.microsoft.com/en-us/windows/win32/winprog/windows-data-types
/// </remarks>
internal readonly struct HMONITOR
{
public static readonly HMONITOR Null = new(IntPtr.Zero);
public readonly IntPtr Value;
public HMONITOR(IntPtr value)
{
this.Value = value;
}
public bool IsNull => this.Value == HMONITOR.Null.Value;
public static implicit operator int(HMONITOR value) => value.Value.ToInt32();
public static explicit operator HMONITOR(int value) => new(value);
public static implicit operator IntPtr(HMONITOR value) => value.Value;
public static explicit operator HMONITOR(IntPtr value) => new(value);
public static implicit operator HANDLE(HMONITOR value) => new(value.Value);
public static explicit operator HMONITOR(HANDLE value) => new(value.Value);
public override string ToString()
{
return $"{this.GetType().Name}({this.Value})";
}
}
}

View File

@@ -1,35 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace MouseJump.Common.NativeMethods;
internal static partial class Core
{
/// <summary>
/// A 32-bit signed integer.The range is -2147483648 through 2147483647 decimal.
/// This type is declared in WinNT.h as follows:
/// typedef long LONG;
/// </summary>
/// <remarks>
/// See https://learn.microsoft.com/en-us/windows/win32/winprog/windows-data-types
/// </remarks>
internal readonly struct LONG
{
public readonly int Value;
public LONG(int value)
{
this.Value = value;
}
public static implicit operator int(LONG value) => value.Value;
public static implicit operator LONG(int value) => new(value);
public override string ToString()
{
return $"{this.GetType().Name}({this.Value})";
}
}
}

View File

@@ -1,50 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Runtime.InteropServices;
namespace MouseJump.Common.NativeMethods;
internal static partial class Core
{
internal readonly struct LPCRECT
{
public static readonly LPCRECT Null = new(IntPtr.Zero);
public readonly IntPtr Value;
public LPCRECT(IntPtr value)
{
this.Value = value;
}
public LPCRECT(CRECT value)
{
this.Value = LPCRECT.ToPtr(value);
}
public bool IsNull => this.Value == LPCRECT.Null.Value;
private static IntPtr ToPtr(CRECT value)
{
var ptr = Marshal.AllocHGlobal(CRECT.Size);
Marshal.StructureToPtr(value, ptr, false);
return ptr;
}
public void Free()
{
Marshal.FreeHGlobal(this.Value);
}
public static implicit operator IntPtr(LPCRECT value) => value.Value;
public static explicit operator LPCRECT(IntPtr value) => new(value);
public override string ToString()
{
return $"{this.GetType().Name}({this.Value})";
}
}
}

View File

@@ -1,55 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Runtime.InteropServices;
namespace MouseJump.Common.NativeMethods;
internal static partial class Core
{
internal readonly struct LPPOINT
{
public static readonly LPPOINT Null = new(IntPtr.Zero);
public readonly IntPtr Value;
public LPPOINT(IntPtr value)
{
this.Value = value;
}
public LPPOINT(POINT value)
{
this.Value = LPPOINT.ToPtr(value);
}
public bool IsNull => this.Value == LPPOINT.Null.Value;
private static IntPtr ToPtr(POINT value)
{
var ptr = Marshal.AllocHGlobal(POINT.Size);
Marshal.StructureToPtr(value, ptr, false);
return ptr;
}
public POINT ToStructure()
{
return Marshal.PtrToStructure<POINT>(this.Value);
}
public void Free()
{
Marshal.FreeHGlobal(this.Value);
}
public static implicit operator IntPtr(LPPOINT value) => value.Value;
public static explicit operator LPPOINT(IntPtr value) => new(value);
public override string ToString()
{
return $"{this.GetType().Name}({this.Value})";
}
}
}

View File

@@ -1,50 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Runtime.InteropServices;
namespace MouseJump.Common.NativeMethods;
internal static partial class Core
{
internal readonly struct LPRECT
{
public static readonly LPRECT Null = new(IntPtr.Zero);
public readonly IntPtr Value;
public LPRECT(IntPtr value)
{
this.Value = value;
}
public LPRECT(RECT value)
{
this.Value = LPRECT.ToPtr(value);
}
public bool IsNull => this.Value == LPRECT.Null.Value;
private static IntPtr ToPtr(RECT value)
{
var ptr = Marshal.AllocHGlobal(RECT.Size);
Marshal.StructureToPtr(value, ptr, false);
return ptr;
}
public void Free()
{
Marshal.FreeHGlobal(this.Value);
}
public static implicit operator IntPtr(LPRECT value) => value.Value;
public static explicit operator LPRECT(IntPtr value) => new(value);
public override string ToString()
{
return $"{this.GetType().Name}({this.Value})";
}
}
}

View File

@@ -1,55 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
namespace MouseJump.Common.NativeMethods;
internal static partial class Core
{
/// <summary>
/// The POINT structure defines the x- and y-coordinates of a point.
/// </summary>
/// <remarks>
/// See https://learn.microsoft.com/en-us/windows/win32/api/windef/ns-windef-point
/// </remarks>
[SuppressMessage("SA1307", "SA1307:AccessibleFieldsMustBeginWithUpperCaseLetter", Justification = "Names match Win32 api")]
internal readonly struct POINT
{
/// <summary>
/// Specifies the x-coordinate of the point.
/// </summary>
public readonly LONG x;
/// <summary>
/// Specifies the y-coordinate of the point.
/// </summary>
public readonly LONG y;
public POINT(
int x,
int y)
{
this.x = x;
this.y = y;
}
public POINT(
LONG x,
LONG y)
{
this.x = x;
this.y = y;
}
public static int Size =>
Marshal.SizeOf(typeof(POINT));
public override string ToString()
{
return $"x={this.x},y={this.y}";
}
}
}

View File

@@ -1,54 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
namespace MouseJump.Common.NativeMethods;
internal static partial class Core
{
/// <summary>
/// The RECT structure defines a rectangle by the coordinates of its upper-left and lower-right corners.
/// </summary>
/// <remarks>
/// See https://learn.microsoft.com/en-us/windows/win32/api/windef/ns-windef-rect
/// </remarks>
[SuppressMessage("Naming Rules", "SA1307:AccessibleFieldsMustBeginWithUpperCaseLetter", Justification = "Name and value taken from Win32Api")]
internal readonly struct RECT
{
public static readonly RECT Empty = new(0, 0, 0, 0);
public readonly LONG left;
public readonly LONG top;
public readonly LONG right;
public readonly LONG bottom;
public RECT(
int left, int top, int right, int bottom)
{
this.left = left;
this.top = top;
this.right = right;
this.bottom = bottom;
}
public RECT(
LONG left, LONG top, LONG right, LONG bottom)
{
this.left = left;
this.top = top;
this.right = right;
this.bottom = bottom;
}
public static int Size =>
Marshal.SizeOf(typeof(RECT));
public override string ToString()
{
return $"left={this.left},top={this.top},right={this.right},bottom={this.bottom}";
}
}
}

View File

@@ -1,39 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace MouseJump.Common.NativeMethods;
internal static partial class Core
{
/// <summary>
/// An unsigned INT. The range is 0 through 4294967295 decimal.
/// This type is declared in WinDef.h as follows:
/// typedef unsigned int UINT;
/// </summary>
/// <remarks>
/// See https://learn.microsoft.com/en-us/windows/win32/winprog/windows-data-types
/// </remarks>
internal readonly struct UINT
{
public readonly uint Value;
public UINT(uint value)
{
this.Value = value;
}
public static implicit operator uint(UINT value) => value.Value;
public static implicit operator UINT(uint value) => new(value);
public static explicit operator int(UINT value) => (int)value.Value;
public static explicit operator UINT(int value) => new((uint)value);
public override string ToString()
{
return $"{this.GetType().Name}({this.Value})";
}
}
}

View File

@@ -1,42 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace MouseJump.Common.NativeMethods;
internal static partial class Core
{
/// <summary>
/// An unsigned LONG_PTR.
/// This type is declared in BaseTsd.h as follows:
/// C++
/// #if defined(_WIN64)
/// typedef unsigned __int64 ULONG_PTR;
/// #else
/// typedef unsigned long ULONG_PTR;
/// #endif
/// </summary>
/// <remarks>
/// See https://learn.microsoft.com/en-us/windows/win32/winprog/windows-data-types
/// </remarks>
internal readonly struct ULONG_PTR
{
public static readonly ULONG_PTR Null = new(UIntPtr.Zero);
public readonly UIntPtr Value;
public ULONG_PTR(UIntPtr value)
{
this.Value = value;
}
public static implicit operator UIntPtr(ULONG_PTR value) => value.Value;
public static explicit operator ULONG_PTR(UIntPtr value) => new(value);
public override string ToString()
{
return $"{this.GetType().Name}({this.Value})";
}
}
}

View File

@@ -1,35 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace MouseJump.Common.NativeMethods;
internal static partial class Core
{
/// <summary>
/// A 16-bit unsigned integer.The range is 0 through 65535 decimal.
/// This type is declared in WinDef.h as follows:
/// typedef unsigned short WORD;
/// </summary>
/// <remarks>
/// See https://learn.microsoft.com/en-us/windows/win32/winprog/windows-data-types
/// </remarks>
internal readonly struct WORD
{
public readonly ushort Value;
public WORD(ushort value)
{
this.Value = value;
}
public static implicit operator ulong(WORD value) => value.Value;
public static implicit operator WORD(ushort value) => new(value);
public override string ToString()
{
return $"{this.GetType().Name}({this.Value})";
}
}
}

View File

@@ -1,37 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace MouseJump.Common.NativeMethods;
internal static partial class Gdi32
{
/// <summary>
/// A raster-operation code. These codes define how the color data for the source
/// rectangle is to be combined with the color data for the destination rectangle
/// to achieve the final color.
/// </summary>
/// <remarks>
/// See https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-bitblt
/// </remarks>
internal enum ROP_CODE : uint
{
BLACKNESS = 0x00000042,
CAPTUREBLT = 0x40000000,
DSTINVERT = 0x00550009,
MERGECOPY = 0x00C000CA,
MERGEPAINT = 0x00BB0226,
NOMIRRORBITMAP = 0x80000000,
NOTSRCCOPY = 0x00330008,
NOTSRCERASE = 0x001100A6,
PATCOPY = 0x00F00021,
PATINVERT = 0x005A0049,
PATPAINT = 0x00FB0A09,
SRCAND = 0x008800C6,
SRCCOPY = 0x00CC0020,
SRCERASE = 0x00440328,
SRCINVERT = 0x00660046,
SRCPAINT = 0x00EE0086,
WHITENESS = 0x00FF0062,
}
}

View File

@@ -1,26 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace MouseJump.Common.NativeMethods;
internal static partial class Gdi32
{
/// <summary>
/// The stretching mode.
/// </summary>
/// <remarks>
/// See https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-setstretchbltmode
/// </remarks>
internal enum STRETCH_BLT_MODE : int
{
BLACKONWHITE = 1,
COLORONCOLOR = 3,
HALFTONE = 4,
WHITEONBLACK = 2,
STRETCH_ANDSCANS = STRETCH_BLT_MODE.BLACKONWHITE,
STRETCH_DELETESCANS = STRETCH_BLT_MODE.COLORONCOLOR,
STRETCH_HALFTONE = STRETCH_BLT_MODE.HALFTONE,
STRETCH_ORSCANS = STRETCH_BLT_MODE.WHITEONBLACK,
}
}

View File

@@ -1,28 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Runtime.InteropServices;
using static MouseJump.Common.NativeMethods.Core;
namespace MouseJump.Common.NativeMethods;
internal static partial class Gdi32
{
/// <summary>
/// The SetStretchBltMode function sets the bitmap stretching mode in the specified device context.
/// </summary>
/// <returns>
/// If the function succeeds, the return value is the previous stretching mode.
/// If the function fails, the return value is zero.
/// This function can return the following value.
/// </returns>
/// <remarks>
/// See https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-setstretchbltmode
/// </remarks>
[LibraryImport(Libraries.Gdi32)]
internal static partial int SetStretchBltMode(
HDC hdc,
STRETCH_BLT_MODE mode);
}

View File

@@ -1,39 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Runtime.InteropServices;
using static MouseJump.Common.NativeMethods.Core;
namespace MouseJump.Common.NativeMethods;
internal static partial class Gdi32
{
/// <summary>
/// The StretchBlt function copies a bitmap from a source rectangle into a destination
/// rectangle, stretching or compressing the bitmap to fit the dimensions of the
/// destination rectangle, if necessary. The system stretches or compresses the bitmap
/// according to the stretching mode currently set in the destination device context.
/// </summary>
/// <returns>
/// If the function succeeds, the return value is nonzero.
/// If the function fails, the return value is zero.
/// </returns>
/// <remarks>
/// See https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-stretchblt
/// </remarks>
[LibraryImport(Libraries.Gdi32)]
internal static partial BOOL StretchBlt(
HDC hdcDest,
int xDest,
int yDest,
int wDest,
int hDest,
HDC hdcSrc,
int xSrc,
int ySrc,
int wSrc,
int hSrc,
ROP_CODE rop);
}

View File

@@ -1,34 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Runtime.InteropServices;
using static MouseJump.Common.NativeMethods.Core;
namespace MouseJump.Common.NativeMethods;
internal static partial class User32
{
/// <summary>
/// The EnumDisplayMonitors function enumerates display monitors (including invisible
/// pseudo-monitors associated with the mirroring drivers) that intersect a region formed
/// by the intersection of a specified clipping rectangle and the visible region of a
/// device context. EnumDisplayMonitors calls an application-defined MonitorEnumProc
/// callback function once for each monitor that is enumerated. Note that
/// GetSystemMetrics (SM_CMONITORS) counts only the display monitors.
/// </summary>
/// <returns>
/// If the function succeeds, the return value is nonzero.
/// If the function fails, the return value is zero.
/// </returns>
/// <remarks>
/// See https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-enumdisplaymonitors
/// </remarks>
[LibraryImport(Libraries.User32)]
internal static partial BOOL EnumDisplayMonitors(
HDC hdc,
LPCRECT lprcClip,
MONITORENUMPROC lpfnEnum,
LPARAM dwData);
}

View File

@@ -1,32 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Runtime.InteropServices;
using static MouseJump.Common.NativeMethods.Core;
namespace MouseJump.Common.NativeMethods;
internal static partial class User32
{
/// <summary>
/// The GetWindowDC function retrieves the device context (DC) for the entire window,
/// including title bar, menus, and scroll bars. A window device context permits painting
/// anywhere in a window, because the origin of the device context is the upper-left
/// corner of the window instead of the client area.
///
/// GetWindowDC assigns default attributes to the window device context each time it
/// retrieves the device context. Previous attributes are lost.
/// </summary>
/// <returns>
/// If the function succeeds, the return value is a handle to a device context for the specified window.
/// If the function fails, the return value is NULL, indicating an error or an invalid hWnd parameter.
/// </returns>
/// <remarks>
/// See https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowdc
/// </remarks>
[LibraryImport(Libraries.User32)]
internal static partial HDC GetWindowDC(
HWND hWnd);
}

View File

@@ -1,52 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Runtime.InteropServices;
namespace MouseJump.Common.NativeMethods;
internal static partial class User32
{
/// <remarks>
/// See https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-sendinput
/// </remarks>
internal readonly struct LPMONITORINFO
{
public static readonly LPMONITORINFO Null = new(IntPtr.Zero);
public readonly IntPtr Value;
public LPMONITORINFO(IntPtr value)
{
this.Value = value;
}
public LPMONITORINFO(MONITORINFO value)
{
this.Value = LPMONITORINFO.ToPtr(value);
}
public MONITORINFO ToStructure()
{
return Marshal.PtrToStructure<MONITORINFO>(this.Value);
}
private static IntPtr ToPtr(MONITORINFO value)
{
var ptr = Marshal.AllocHGlobal(MONITORINFO.Size);
Marshal.StructureToPtr(value, ptr, false);
return ptr;
}
public void Free()
{
Marshal.FreeHGlobal(this.Value);
}
public override string ToString()
{
return $"{this.GetType().Name}({this.Value})";
}
}
}

View File

@@ -1,22 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using static MouseJump.Common.NativeMethods.Core;
namespace MouseJump.Common.NativeMethods;
internal static partial class User32
{
/// <summary>
/// A MonitorEnumProc function is an application-defined callback function that is called by the EnumDisplayMonitors function.
/// </summary>
/// <remarks>
/// See https://learn.microsoft.com/en-us/windows/win32/api/winuser/nc-winuser-monitorenumproc
/// </remarks>
internal delegate BOOL MONITORENUMPROC(
HMONITOR unnamedParam1,
HDC unnamedParam2,
LPRECT unnamedParam3,
LPARAM unnamedParam4);
}

View File

@@ -1,40 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using static MouseJump.Common.NativeMethods.Core;
namespace MouseJump.Common.NativeMethods;
internal static partial class User32
{
/// <summary>
/// Used by SendInput to store information for synthesizing input events such as keystrokes, mouse movement, and mouse clicks.
/// </summary>
/// <remarks>
/// See https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-monitorinfo
/// </remarks>
[SuppressMessage("SA1307", "SA1307:AccessibleFieldsMustBeginWithUpperCaseLetter", Justification = "Parameter name matches Win32 api")]
[StructLayout(LayoutKind.Sequential)]
internal readonly struct MONITORINFO
{
public readonly DWORD cbSize;
public readonly RECT rcMonitor;
public readonly RECT rcWork;
public readonly MONITOR_INFO_FLAGS dwFlags;
public MONITORINFO(DWORD cbSize, RECT rcMonitor, RECT rcWork, MONITOR_INFO_FLAGS dwFlags)
{
this.cbSize = cbSize;
this.rcMonitor = rcMonitor;
this.rcWork = rcWork;
this.dwFlags = dwFlags;
}
public static int Size =>
Marshal.SizeOf(typeof(MONITORINFO));
}
}

View File

@@ -1,24 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Diagnostics.CodeAnalysis;
namespace MouseJump.Common.NativeMethods;
[SuppressMessage("SA1310", "SA1310:FieldNamesMustNotContainUnderscore", Justification = "Names match Win32 api")]
internal static partial class User32
{
/// <summary>
/// Determines the function's return value if the point is not contained within any display monitor.
/// </summary>
/// <remarks>
/// See https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-monitorfrompoint
/// </remarks>
internal enum MONITOR_FROM_FLAGS : uint
{
MONITOR_DEFAULTTONULL = 0x00000000,
MONITOR_DEFAULTTOPRIMARY = 0x00000001,
MONITOR_DEFAULTTONEAREST = 0x00000002,
}
}

View File

@@ -1,22 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Diagnostics.CodeAnalysis;
namespace MouseJump.Common.NativeMethods;
[SuppressMessage("SA1310", "SA1310:FieldNamesMustNotContainUnderscore", Justification = "Names match Win32 api")]
internal static partial class User32
{
/// <summary>
/// A set of flags that represent attributes of the display monitor.
/// </summary>
/// <remarks>
/// See https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-monitorinfo
/// </remarks>
internal enum MONITOR_INFO_FLAGS : uint
{
MONITORINFOF_PRIMARY = 1,
}
}

View File

@@ -1,27 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Runtime.InteropServices;
using static MouseJump.Common.NativeMethods.Core;
namespace MouseJump.Common.NativeMethods;
internal static partial class User32
{
/// <summary>
/// The MonitorFromPoint function retrieves a handle to the display monitor that contains a specified point.
/// </summary>
/// <returns>
/// If the point is contained by a display monitor, the return value is an HMONITOR handle to that display monitor.
/// If the point is not contained by a display monitor, the return value depends on the value of dwFlags.
/// </returns>
/// <remarks>
/// See https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-monitorfrompoint
/// </remarks>
[LibraryImport(Libraries.User32)]
internal static partial HMONITOR MonitorFromPoint(
POINT pt,
MONITOR_FROM_FLAGS dwFlags);
}

View File

@@ -1,29 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Runtime.InteropServices;
using static MouseJump.Common.NativeMethods.Core;
namespace MouseJump.Common.NativeMethods;
internal static partial class User32
{
/// <summary>
/// The ReleaseDC function releases a device context (DC), freeing it for use by other
/// applications. The effect of the ReleaseDC function depends on the type of DC. It
/// frees only common and window DCs. It has no effect on class or private DCs.
/// </summary>
/// <returns>
/// The return value indicates whether the DC was released. If the DC was released, the return value is 1.
/// If the DC was not released, the return value is zero.
/// </returns>
/// <remarks>
/// See https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-releasedc
/// </remarks>
[LibraryImport(Libraries.User32)]
internal static partial int ReleaseDC(
HWND hWnd,
HDC hDC);
}

View File

@@ -1,39 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using static MouseJump.Common.NativeMethods.Core;
namespace MouseJump.Common.NativeMethods;
internal static partial class User32
{
/// <summary>
/// Contains information about a simulated message generated by an input device
/// other than a keyboard or mouse.
/// </summary>
/// <remarks>
/// See https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-hardwareinput
/// </remarks>
[SuppressMessage("SA1307", "SA1307:AccessibleFieldsMustBeginWithUpperCaseLetter", Justification = "Parameter name matches Win32 api")]
[StructLayout(LayoutKind.Sequential)]
internal readonly struct HARDWAREINPUT
{
public readonly DWORD uMsg;
public readonly WORD wParamL;
public readonly WORD wParamH;
public HARDWAREINPUT(
DWORD uMsg,
WORD wParamL,
WORD wParamH)
{
this.uMsg = uMsg;
this.wParamL = wParamL;
this.wParamH = wParamH;
}
}
}

View File

@@ -1,50 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
namespace MouseJump.Common.NativeMethods;
internal static partial class User32
{
/// <summary>
/// Used by SendInput to store information for synthesizing input events such as keystrokes, mouse movement, and mouse clicks.
/// </summary>
/// <remarks>
/// See https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-input
/// </remarks>
[SuppressMessage("SA1307", "SA1307:AccessibleFieldsMustBeginWithUpperCaseLetter", Justification = "Parameter name matches Win32 api")]
[StructLayout(LayoutKind.Sequential)]
internal readonly struct INPUT
{
public readonly INPUT_TYPE type;
public readonly DUMMYUNIONNAME data;
public INPUT(INPUT_TYPE type, DUMMYUNIONNAME data)
{
this.type = type;
this.data = data;
}
public static int Size =>
Marshal.SizeOf(typeof(INPUT));
[StructLayout(LayoutKind.Explicit)]
public readonly struct DUMMYUNIONNAME
{
[FieldOffset(0)]
public readonly MOUSEINPUT mi;
[FieldOffset(0)]
public readonly KEYBDINPUT ki;
[FieldOffset(0)]
public readonly HARDWAREINPUT hi;
public DUMMYUNIONNAME(MOUSEINPUT mi)
{
this.mi = mi;
}
}
}
}

View File

@@ -1,24 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Diagnostics.CodeAnalysis;
namespace MouseJump.Common.NativeMethods;
[SuppressMessage("SA1310", "SA1310:FieldNamesMustNotContainUnderscore", Justification = "Names match Win32 api")]
internal static partial class User32
{
/// <summary>
/// The type of the input event.
/// </summary>
/// <remarks>
/// See https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-input
/// </remarks>
internal enum INPUT_TYPE : uint
{
INPUT_MOUSE = 0,
INPUT_KEYBOARD = 1,
INPUT_HARDWARE = 2,
}
}

View File

@@ -1,44 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using static MouseJump.Common.NativeMethods.Core;
namespace MouseJump.Common.NativeMethods;
internal static partial class User32
{
/// <summary>
/// Contains information about a simulated keyboard event.
/// </summary>
/// <remarks>
/// See https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-keybdinput
/// </remarks>
[SuppressMessage("SA1307", "SA1307:AccessibleFieldsMustBeginWithUpperCaseLetter", Justification = "Parameter name matches Win32 api")]
[StructLayout(LayoutKind.Sequential)]
internal readonly struct KEYBDINPUT
{
public readonly WORD wVk;
public readonly WORD wScan;
public readonly DWORD dwFlags;
public readonly DWORD time;
public readonly ULONG_PTR dwExtraInfo;
public KEYBDINPUT(
WORD wVk,
WORD wScan,
DWORD dwFlags,
DWORD time,
ULONG_PTR dwExtraInfo)
{
this.wVk = wVk;
this.wScan = wScan;
this.dwFlags = dwFlags;
this.time = time;
this.dwExtraInfo = dwExtraInfo;
}
}
}

View File

@@ -1,70 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Runtime.InteropServices;
namespace MouseJump.Common.NativeMethods;
internal static partial class User32
{
/// <remarks>
/// See https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-sendinput
/// </remarks>
internal readonly struct LPINPUT
{
public static readonly LPINPUT Null = new(IntPtr.Zero);
public readonly IntPtr Value;
public LPINPUT(IntPtr value)
{
this.Value = value;
}
public LPINPUT(INPUT[] values)
{
this.Value = LPINPUT.ToPtr(values);
}
public INPUT ToStructure()
{
return Marshal.PtrToStructure<INPUT>(this.Value);
}
public IEnumerable<INPUT> ToStructure(int count)
{
var ptr = this.Value;
var size = INPUT.Size;
for (var i = 0; i < count; i++)
{
yield return Marshal.PtrToStructure<INPUT>(this.Value);
ptr += size;
}
}
private static IntPtr ToPtr(INPUT[] values)
{
var mem = Marshal.AllocHGlobal(INPUT.Size * values.Length);
var ptr = mem;
var size = INPUT.Size;
foreach (var value in values)
{
Marshal.StructureToPtr(value, ptr, false);
ptr += size;
}
return mem;
}
public void Free()
{
Marshal.FreeHGlobal(this.Value);
}
public override string ToString()
{
return $"{this.GetType().Name}({this.Value})";
}
}
}

View File

@@ -1,47 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using static MouseJump.Common.NativeMethods.Core;
namespace MouseJump.Common.NativeMethods;
internal static partial class User32
{
/// <summary>
/// Contains information about a simulated mouse event.
/// </summary>
/// <remarks>
/// See https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-mouseinput
/// </remarks>
[SuppressMessage("SA1307", "SA1307:AccessibleFieldsMustBeginWithUpperCaseLetter", Justification = "Parameter name matches Win32 api")]
[StructLayout(LayoutKind.Sequential)]
internal readonly struct MOUSEINPUT
{
public readonly int dx;
public readonly int dy;
public readonly DWORD mouseData;
public readonly MOUSE_EVENT_FLAGS dwFlags;
public readonly DWORD time;
public readonly ULONG_PTR dwExtraInfo;
public MOUSEINPUT(
int dx,
int dy,
DWORD mouseData,
MOUSE_EVENT_FLAGS dwFlags,
DWORD time,
ULONG_PTR dwExtraInfo)
{
this.dx = dx;
this.dy = dy;
this.mouseData = mouseData;
this.dwFlags = dwFlags;
this.time = time;
this.dwExtraInfo = dwExtraInfo;
}
}
}

View File

@@ -1,33 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Diagnostics.CodeAnalysis;
namespace MouseJump.Common.NativeMethods;
[SuppressMessage("SA1310", "SA1310:FieldNamesMustNotContainUnderscore", Justification = "Names match Win32 api")]
internal static partial class User32
{
/// <remarks>
/// See https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-mouseinput
/// </remarks>
[Flags]
internal enum MOUSE_EVENT_FLAGS : uint
{
MOUSEEVENTF_MOVE = 0x0001,
MOUSEEVENTF_LEFTDOWN = 0x0002,
MOUSEEVENTF_LEFTUP = 0x0004,
MOUSEEVENTF_RIGHTDOWN = 0x0008,
MOUSEEVENTF_RIGHTUP = 0x0010,
MOUSEEVENTF_MIDDLEDOWN = 0x0020,
MOUSEEVENTF_MIDDLEUP = 0x0040,
MOUSEEVENTF_XDOWN = 0x0080,
MOUSEEVENTF_XUP = 0x0100,
MOUSEEVENTF_WHEEL = 0x0800,
MOUSEEVENTF_HWHEEL = 0x1000,
MOUSEEVENTF_MOVE_NOCOALESCE = 0x2000,
MOUSEEVENTF_VIRTUALDESK = 0x4000,
MOUSEEVENTF_ABSOLUTE = 0x8000,
}
}

View File

@@ -1,29 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Runtime.InteropServices;
using static MouseJump.Common.NativeMethods.Core;
namespace MouseJump.Common.NativeMethods;
internal static partial class User32
{
/// <summary>
/// Synthesizes keystrokes, mouse motions, and button clicks.
/// </summary>
/// <returns>
/// The function returns the number of events that it successfully inserted into the keyboard or mouse input stream.
/// If the function returns zero, the input was already blocked by another thread.
/// To get extended error information, call GetLastError.
/// </returns>
/// <remarks>
/// See https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-sendinput
/// </remarks>
[LibraryImport(Libraries.User32, SetLastError = true)]
internal static partial UINT SendInput(
UINT cInputs,
LPINPUT pInputs,
int cbSize);
}

View File

@@ -1,26 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Runtime.InteropServices;
using static MouseJump.Common.NativeMethods.Core;
namespace MouseJump.Common.NativeMethods;
internal static partial class User32
{
/// <summary>
/// Retrieves the position of the mouse cursor, in screen coordinates.
/// </summary>
/// <returns>
/// Returns nonzero if successful or zero otherwise.
/// To get extended error information, call GetLastError.
/// </returns>
/// <remarks>
/// See https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getcursorpos
/// </remarks>
[LibraryImport(Libraries.User32, SetLastError = true)]
internal static partial BOOL GetCursorPos(
LPPOINT lpPoint);
}

View File

@@ -1,25 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Runtime.InteropServices;
using static MouseJump.Common.NativeMethods.Core;
namespace MouseJump.Common.NativeMethods;
internal static partial class User32
{
/// <summary>
/// Retrieves a handle to the desktop window. The desktop window covers the entire
/// screen. The desktop window is the area on top of which other windows are painted.
/// </summary>
/// <returns>
/// The return value is a handle to the desktop window.
/// </returns>
/// <remarks>
/// See https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getdesktopwindow
/// </remarks>
[LibraryImport(Libraries.User32)]
internal static partial HWND GetDesktopWindow();
}

View File

@@ -1,26 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Runtime.InteropServices;
namespace MouseJump.Common.NativeMethods;
internal static partial class User32
{
/// <summary>
/// Retrieves the specified system metric or system configuration setting.
///
/// Note that all dimensions retrieved by GetSystemMetrics are in pixels.
/// </summary>
/// <returns>
/// If the function succeeds, the return value is the requested system metric or configuration setting.
/// If the function fails, the return value is 0. GetLastError does not provide extended error information.
/// </returns>
/// <remarks>
/// See https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getsystemmetrics
/// </remarks>
[LibraryImport(Libraries.User32)]
internal static partial int GetSystemMetrics(
SYSTEM_METRICS_INDEX smIndex);
}

View File

@@ -1,110 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Diagnostics.CodeAnalysis;
namespace MouseJump.Common.NativeMethods;
[SuppressMessage("SA1310", "SA1310:FieldNamesMustNotContainUnderscore", Justification = "Names match Win32 api")]
internal static partial class User32
{
internal enum SYSTEM_METRICS_INDEX : uint
{
SM_ARRANGE = 56,
SM_CLEANBOOT = 67,
SM_CMONITORS = 80,
SM_CMOUSEBUTTONS = 43,
SM_CONVERTIBLESLATEMODE = 0x2003,
SM_CXBORDER = 5,
SM_CXCURSOR = 13,
SM_CXDLGFRAME = 7,
SM_CXDOUBLECLK = 36,
SM_CXDRAG = 68,
SM_CXEDGE = 45,
SM_CXFIXEDFRAME = SM_CXDLGFRAME,
SM_CXFOCUSBORDER = 83,
SM_CXFRAME = 32,
SM_CXFULLSCREEN = 16,
SM_CXHSCROLL = 21,
SM_CXHTHUMB = 10,
SM_CXICON = 11,
SM_CXICONSPACING = 38,
SM_CXMAXIMIZED = 61,
SM_CXMAXTRACK = 59,
SM_CXMENUCHECK = 71,
SM_CXMENUSIZE = 54,
SM_CXMIN = 28,
SM_CXMINIMIZED = 57,
SM_CXMINSPACING = 47,
SM_CXMINTRACK = 34,
SM_CXPADDEDBORDER = 92,
SM_CXSCREEN = 0,
SM_CXSIZE = 30,
SM_CXSIZEFRAME = SM_CXFRAME,
SM_CXSMICON = 49,
SM_CXSMSIZE = 52,
SM_CXVIRTUALSCREEN = 78,
SM_CXVSCROLL = 2,
SM_CYBORDER = 6,
SM_CYCAPTION = 4,
SM_CYCURSOR = 14,
SM_CYDLGFRAME = 8,
SM_CYDOUBLECLK = 37,
SM_CYDRAG = 69,
SM_CYEDGE = 46,
SM_CYFIXEDFRAME = SM_CYDLGFRAME,
SM_CYFOCUSBORDER = 84,
SM_CYFRAME = 33,
SM_CYFULLSCREEN = 17,
SM_CYHSCROLL = 3,
SM_CYICON = 12,
SM_CYICONSPACING = 39,
SM_CYKANJIWINDOW = 18,
SM_CYMAXIMIZED = 62,
SM_CYMAXTRACK = 60,
SM_CYMENU = 15,
SM_CYMENUCHECK = 72,
SM_CYMENUSIZE = 55,
SM_CYMIN = 29,
SM_CYMINIMIZED = 58,
SM_CYMINSPACING = 48,
SM_CYMINTRACK = 35,
SM_CYSCREEN = 1,
SM_CYSIZE = 31,
SM_CYSIZEFRAME = SM_CYFRAME,
SM_CYSMCAPTION = 51,
SM_CYSMICON = 50,
SM_CYSMSIZE = 53,
SM_CYVIRTUALSCREEN = 79,
SM_CYVSCROLL = 20,
SM_CYVTHUMB = 9,
SM_DBCSENABLED = 42,
SM_DEBUG = 22,
SM_DIGITIZER = 94,
SM_IMMENABLED = 82,
SM_MAXIMUMTOUCHES = 95,
SM_MEDIACENTER = 87,
SM_MENUDROPALIGNMENT = 40,
SM_MIDEASTENABLED = 74,
SM_MOUSEPRESENT = 19,
SM_MOUSEHORIZONTALWHEELPRESENT = 91,
SM_MOUSEWHEELPRESENT = 75,
SM_NETWORK = 63,
SM_PENWINDOWS = 41,
SM_REMOTECONTROL = 0x2001,
SM_REMOTESESSION = 0x1000,
SM_SAMEDISPLAYFORMA = 81,
SM_SECURE = 44,
SM_SERVERR2 = 89,
SM_SHOWSOUNDS = 70,
SM_SHUTTINGDOWN = 0x2000,
SM_SLOWMACHINE = 73,
SM_STARTER = 88,
SM_SWAPBUTTON = 23,
SM_SYSTEMDOCKED = 0x2004,
SM_TABLETPC = 86,
SM_XVIRTUALSCREEN = 76,
SM_YVIRTUALSCREEN = 77,
}
}

View File

@@ -1,29 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Runtime.InteropServices;
using static MouseJump.Common.NativeMethods.Core;
namespace MouseJump.Common.NativeMethods;
internal static partial class User32
{
/// <summary>
/// Moves the cursor to the specified screen coordinates. If the new coordinates are not within
/// the screen rectangle set by the most recent ClipCursor function call, the system automatically
/// adjusts the coordinates so that the cursor stays within the rectangle.
/// </summary>
/// <returns>
/// Returns nonzero if successful or zero otherwise.
/// To get extended error information, call GetLastError.
/// </returns>
/// <remarks>
/// See https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getcursorpos
/// </remarks>
[LibraryImport(Libraries.User32, SetLastError = true)]
internal static partial BOOL SetCursorPos(
int X,
int Y);
}

View File

@@ -0,0 +1,61 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace MouseJump.HotKeys.UnitTests;
public static class KeystrokeTests
{
[TestClass]
public sealed class ParseTests
{
public sealed class TestCase
{
public TestCase(string value, Keystroke expectedResult)
{
this.Value = value;
this.ExpectedResult = expectedResult;
}
public string Value { get; }
public Keystroke ExpectedResult { get; }
}
public static IEnumerable<object[]> GetTestCases()
{
// individual modifiers / keys
yield return new object[] { new TestCase("CTRL", new(Keys.None, KeyModifiers.Control)) };
yield return new object[] { new TestCase("ALT", new(Keys.None, KeyModifiers.Alt)) };
yield return new object[] { new TestCase("SHIFT", new(Keys.None, KeyModifiers.Shift)) };
yield return new object[] { new TestCase("WIN", new(Keys.None, KeyModifiers.Windows)) };
yield return new object[] { new TestCase("F", new(Keys.F, KeyModifiers.None)) };
// multiple modifiers
yield return new object[] { new TestCase("CTRL + ALT + SHIFT", new(Keys.None, KeyModifiers.Control | KeyModifiers.Alt | KeyModifiers.Shift)) };
yield return new object[] { new TestCase("SHIFT + ALT + CTRL", new(Keys.None, KeyModifiers.Control | KeyModifiers.Alt | KeyModifiers.Shift)) };
// modifiers and keys
yield return new object[] { new TestCase("CTRL + ALT + SHIFT + F", new(Keys.F, KeyModifiers.Control | KeyModifiers.Alt | KeyModifiers.Shift)) };
yield return new object[] { new TestCase("CTRL + ALT + SHIFT + WIN + F", new(Keys.F, KeyModifiers.Control | KeyModifiers.Alt | KeyModifiers.Shift | KeyModifiers.Windows)) };
}
[TestMethod]
[DynamicData(nameof(GetTestCases))]
public void RunTestCases(TestCase data)
{
var expected = data.ExpectedResult;
var result = Keystroke.TryParse(data.Value, out var actual);
Assert.IsTrue(result);
if (actual == null)
{
throw new InvalidOperationException();
}
Assert.AreEqual(expected.Key, actual.Key);
Assert.AreEqual(expected.Modifiers, actual.Modifiers);
}
}
}

View File

@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="$(RepoRoot)src\Common.Dotnet.CsWinRT.props" />
<PropertyGroup>
<ProjectGuid>{C1434F2F-D34A-4E4E-9471-4041066B8EC4}</ProjectGuid>
<AssemblyName>PowerToys.MouseJump.HotKeys.UnitTests</AssemblyName>
<AssemblyTitle>PowerToys.MouseJump.HotKeys.UnitTests</AssemblyTitle>
<AssemblyDescription>PowerToys MouseJump.HotKeys.UnitTests</AssemblyDescription>
<OutputType>Library</OutputType>
<OutputPath>..\..\..\..\$(Platform)\$(Configuration)\tests\MouseJump.HotKeys.UnitTests\</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\MouseJump.HotKeys\MouseJump.HotKeys.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="MSTest" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,18 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Windows.Win32.UI.Input.KeyboardAndMouse;
namespace MouseJump.HotKeys;
[Flags]
public enum KeyModifiers
{
None = 0,
Alt = (int)HOT_KEY_MODIFIERS.MOD_ALT,
Control = (int)HOT_KEY_MODIFIERS.MOD_CONTROL,
Shift = (int)HOT_KEY_MODIFIERS.MOD_SHIFT,
Windows = (int)HOT_KEY_MODIFIERS.MOD_WIN,
NoRepeat = (int)HOT_KEY_MODIFIERS.MOD_NOREPEAT,
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,120 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Diagnostics.CodeAnalysis;
namespace MouseJump.HotKeys;
public sealed class Keystroke
{
public Keystroke(Keys key, KeyModifiers modifiers)
{
this.Key = key;
this.Modifiers = modifiers;
}
public Keys Key
{
get;
}
public KeyModifiers Modifiers
{
get;
}
public static Keystroke Parse(string s)
{
if (!Keystroke.TryParse(s, out var result))
{
throw new ArgumentException("Invalid argument format.", nameof(s));
}
return result;
}
public static bool TryParse(string s, [NotNullWhen(true)] out Keystroke? result)
{
// see https://github.com/microsoft/terminal/blob/14919073a12fc0ecb4a9805cc183fdd68d30c4b6/src/cascadia/TerminalSettingsModel/KeyChordSerialization.cpp#L124
// for an alternate implementation
// e.g. "CTRL + ALT + SHIFT + F"
if (string.IsNullOrEmpty(s))
{
result = null;
return false;
}
var parts = s
.Replace(" ", string.Empty)
.ToUpperInvariant()
.Split('+');
var keystroke = (Keys: Keys.None, Modifiers: KeyModifiers.None);
foreach (var part in parts)
{
switch (part)
{
case "CTRL":
keystroke.Modifiers |= KeyModifiers.Control;
break;
case "ALT":
keystroke.Modifiers |= KeyModifiers.Alt;
break;
case "SHIFT":
keystroke.Modifiers |= KeyModifiers.Shift;
break;
case "WIN":
keystroke.Modifiers |= KeyModifiers.Windows;
break;
default:
if (!Enum.TryParse<Keys>(part, out var key))
{
result = null;
return false;
}
keystroke.Keys = key;
break;
}
}
result = new Keystroke(
keystroke.Keys, keystroke.Modifiers);
return true;
}
public override string ToString()
{
var parts = new List<string>();
if (this.Modifiers.HasFlag(KeyModifiers.Control))
{
parts.Add("CTRL");
}
if (this.Modifiers.HasFlag(KeyModifiers.Alt))
{
parts.Add("ALT");
}
if (this.Modifiers.HasFlag(KeyModifiers.Shift))
{
parts.Add("SHIFT");
}
if (this.Modifiers.HasFlag(KeyModifiers.Windows))
{
parts.Add("WIN");
}
if (this.Key != Keys.None)
{
parts.Add(this.Key.ToString());
}
return string.Join(" + ", parts);
}
}

View File

@@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="$(RepoRoot)src\Common.Dotnet.CsWinRT.props" />
<PropertyGroup>
<ProjectGuid>{A8D81CE0-7E24-4079-AD45-6BCE6747FE8B}</ProjectGuid>
<AssemblyName>PowerToys.MouseJump.HotKeys</AssemblyName>
<AssemblyTitle>PowerToys.MouseJump.HotKeys</AssemblyTitle>
<AssemblyDescription>PowerToys MouseJump.HotKeys</AssemblyDescription>
<OutputType>Library</OutputType>
<OutputPath>$(RepoRoot)$(Platform)\$(Configuration)\WinUI3Apps</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<ApplicationHighDpiMode>PerMonitorV2</ApplicationHighDpiMode>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\MouseJump.Common\MouseJump.Common.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Windows.CsWin32">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>

View File

@@ -0,0 +1 @@
HOT_KEY_MODIFIERS

View File

@@ -0,0 +1,116 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Drawing;
using System.Text.Json;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MouseJump.Models.Drawing;
using MouseJump.Models.Styles;
namespace MouseJump.Models.UnitTests.Drawing;
public static class BoxBoundsTests
{
[TestClass]
public sealed class CreateFromContentBounds
{
public sealed class TestCase
{
public TestCase(RectangleInfo contentBounds, BoxStyle boxStyle, BoxBounds expectedResult)
{
this.ContentBounds = contentBounds;
this.BoxStyle = boxStyle;
this.ExpectedResult = expectedResult;
}
public RectangleInfo ContentBounds { get; }
public BoxStyle BoxStyle { get; }
public BoxBounds ExpectedResult { get; }
}
public static IEnumerable<object[]> GetTestCases()
{
yield return new object[]
{
new TestCase(
contentBounds: new(100, 100, 800, 600),
boxStyle: new(
marginStyle: new(3),
borderStyle: new(Color.Red, 5, 0),
paddingStyle: new(7),
backgroundStyle: BackgroundStyle.Empty),
expectedResult: new(
outerBounds: new(85, 85, 830, 630),
marginBounds: new(85, 85, 830, 630),
borderBounds: new(88, 88, 824, 624),
paddingBounds: new(93, 93, 814, 614),
contentBounds: new(100, 100, 800, 600))),
};
}
[TestMethod]
[DynamicData(nameof(GetTestCases))]
public void RunTestCases(TestCase data)
{
var actual = BoxBounds.CreateFromContentBounds(data.ContentBounds, data.BoxStyle);
var expected = data.ExpectedResult;
Assert.AreEqual(
JsonSerializer.Serialize(expected),
JsonSerializer.Serialize(actual));
}
}
[TestClass]
public sealed class CreateFromOuterBounds
{
public sealed class TestCase
{
public TestCase(RectangleInfo outerBounds, BoxStyle boxStyle, BoxBounds expectedResult)
{
this.OuterBounds = outerBounds;
this.BoxStyle = boxStyle;
this.ExpectedResult = expectedResult;
}
public RectangleInfo OuterBounds { get; }
public BoxStyle BoxStyle { get; }
public BoxBounds ExpectedResult { get; }
}
public static IEnumerable<object[]> GetTestCases()
{
yield return new object[]
{
new TestCase(
outerBounds: new(85, 85, 830, 630),
boxStyle: new(
marginStyle: new(3),
borderStyle: new(Color.Red, 5, 0),
paddingStyle: new(7),
backgroundStyle: BackgroundStyle.Empty),
expectedResult: new(
outerBounds: new(85, 85, 830, 630),
marginBounds: new(85, 85, 830, 630),
borderBounds: new(88, 88, 824, 624),
paddingBounds: new(93, 93, 814, 614),
contentBounds: new(100, 100, 800, 600))),
};
}
[TestMethod]
[DynamicData(nameof(GetTestCases))]
public void RunTestCases(TestCase data)
{
var actual = BoxBounds.CreateFromOuterBounds(data.OuterBounds, data.BoxStyle);
var expected = data.ExpectedResult;
Assert.AreEqual(
JsonSerializer.Serialize(expected),
JsonSerializer.Serialize(actual));
}
}
}

View File

@@ -3,11 +3,10 @@
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.VisualStudio.TestTools.UnitTesting;
using MouseJump.Common.Models.Drawing; using MouseJump.Models.Drawing;
namespace MouseJump.Common.UnitTests.Models.Drawing; namespace MouseJump.Models.UnitTests.Drawing;
[TestClass]
public static class RectangleInfoTests public static class RectangleInfoTests
{ {
[TestClass] [TestClass]
@@ -49,7 +48,7 @@ public static class RectangleInfoTests
} }
[TestMethod] [TestMethod]
[DynamicData(nameof(GetTestCases), DynamicDataSourceType.Method)] [DynamicData(nameof(GetTestCases))]
public void RunTestCases(TestCase data) public void RunTestCases(TestCase data)
{ {
var actual = data.Rectangle.Center(data.Point); var actual = data.Rectangle.Center(data.Point);
@@ -62,7 +61,7 @@ public static class RectangleInfoTests
} }
[TestClass] [TestClass]
public sealed class ClampTests public sealed class MoveInsideTests
{ {
public sealed class TestCase public sealed class TestCase
{ {
@@ -126,10 +125,10 @@ public static class RectangleInfoTests
} }
[TestMethod] [TestMethod]
[DynamicData(nameof(GetTestCases), DynamicDataSourceType.Method)] [DynamicData(nameof(GetTestCases))]
public void RunTestCases(TestCase data) public void RunTestCases(TestCase data)
{ {
var actual = data.Inner.Clamp(data.Outer); var actual = data.Inner.MoveInside(data.Outer);
var expected = data.ExpectedResult; var expected = data.ExpectedResult;
Assert.AreEqual(expected.X, actual.X); Assert.AreEqual(expected.X, actual.X);
Assert.AreEqual(expected.Y, actual.Y); Assert.AreEqual(expected.Y, actual.Y);

View File

@@ -3,11 +3,10 @@
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.VisualStudio.TestTools.UnitTesting;
using MouseJump.Common.Models.Drawing; using MouseJump.Models.Drawing;
namespace MouseJump.Common.UnitTests.Models.Drawing; namespace MouseJump.Models.UnitTests.Drawing;
[TestClass]
public static class SizeInfoTests public static class SizeInfoTests
{ {
[TestClass] [TestClass]
@@ -50,7 +49,7 @@ public static class SizeInfoTests
} }
[TestMethod] [TestMethod]
[DynamicData(nameof(GetTestCases), DynamicDataSourceType.Method)] [DynamicData(nameof(GetTestCases))]
public void RunTestCases(TestCase data) public void RunTestCases(TestCase data)
{ {
var actual = data.Source.ScaleToFit(data.Bounds, out var scalingRatio); var actual = data.Source.ScaleToFit(data.Bounds, out var scalingRatio);
@@ -98,7 +97,7 @@ public static class SizeInfoTests
} }
[TestMethod] [TestMethod]
[DynamicData(nameof(GetTestCases), DynamicDataSourceType.Method)] [DynamicData(nameof(GetTestCases))]
public void RunTestCases(TestCase data) public void RunTestCases(TestCase data)
{ {
var actual = data.Obj.ScaleToFitRatio(data.Bounds); var actual = data.Obj.ScaleToFitRatio(data.Bounds);

View File

@@ -0,0 +1,7 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.VisualStudio.TestTools.UnitTesting;
[assembly: Parallelize(Scope = ExecutionScope.MethodLevel)]

View File

@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="$(RepoRoot)src\Common.Dotnet.CsWinRT.props" />
<PropertyGroup>
<ProjectGuid>{AB6305DB-CA33-4EF4-BA40-B0CFC10E7B9F}</ProjectGuid>
<AssemblyName>PowerToys.MouseJump.Models.UnitTests</AssemblyName>
<AssemblyTitle>PowerToys.MouseJump.Models.UnitTests</AssemblyTitle>
<AssemblyDescription>PowerToys MouseJump.Models.UnitTests</AssemblyDescription>
<OutputType>Library</OutputType>
<OutputPath>..\..\..\..\$(Platform)\$(Configuration)\tests\MouseJump.Models.UnitTests\</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\MouseJump.Models\MouseJump.Models.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="MSTest" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,32 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.ObjectModel;
using MouseJump.Models.Drawing;
namespace MouseJump.Models.Display;
/// <summary>
/// Represents the entire desktop or virtual screen for a single logical device.
/// </summary>
public sealed record DesktopInfo
{
public DesktopInfo(IEnumerable<ScreenInfo> screens)
{
this.Screens = (screens ?? throw new ArgumentNullException(nameof(screens))).ToList().AsReadOnly();
}
public ReadOnlyCollection<ScreenInfo> Screens
{
get;
}
public RectangleInfo GetCombinedDisplayArea()
{
return (this.Screens.Count == 0)
? throw new InvalidOperationException($"{nameof(GetCombinedDisplayArea)} requires one or more screens.")
: RectangleInfo.Union(this.Screens.Select(screen => screen.DisplayArea));
}
}

View File

@@ -0,0 +1,42 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.ObjectModel;
using MouseJump.Models.Drawing;
namespace MouseJump.Models.Display;
/// <summary>
/// Represents a device whose screens are rendered in the preview image.
/// </summary>
public sealed record DeviceInfo
{
public DeviceInfo(string hostname, bool localhost, IEnumerable<ScreenInfo> screens)
{
this.Hostname = hostname ?? throw new ArgumentNullException(nameof(hostname));
this.Localhost = localhost;
this.Screens = (screens ?? throw new ArgumentNullException(nameof(screens))).ToList().AsReadOnly();
}
public string Hostname
{
get;
}
public bool Localhost
{
get;
}
public ReadOnlyCollection<ScreenInfo> Screens
{
get;
}
public RectangleInfo GetCombinedDisplayArea()
{
return RectangleInfo.Union(this.Screens.Select(screen => screen.DisplayArea));
}
}

View File

@@ -0,0 +1,20 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.ObjectModel;
namespace MouseJump.Models.Display;
public sealed record DisplayInfo
{
public DisplayInfo(IEnumerable<DeviceInfo> devices)
{
this.Devices = (devices ?? throw new ArgumentNullException(nameof(devices))).ToList().AsReadOnly();
}
public ReadOnlyCollection<DeviceInfo> Devices
{
get;
}
}

View File

@@ -0,0 +1,17 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace MouseJump.Models.Display;
/// <summary>
/// See https://github.com/dotnet/winforms/blob/main/src/System.Windows.Forms.Primitives/src/System/Windows/Forms/HighDpiMode.cs#L26
/// </summary>
public enum HighDpiMode
{
DpiUnaware,
SystemAware,
PerMonitor,
PerMonitorV2,
DpiUnawareGdiScaled,
}

View File

@@ -2,40 +2,49 @@
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
namespace MouseJump.Common.Models.Drawing; using System.Text.Json.Serialization;
using MouseJump.Models.Drawing;
namespace MouseJump.Models.Display;
/// <summary> /// <summary>
/// Immutable version of a System.Windows.Forms.Screen object so we don't need to /// Immutable version of a System.Windows.Forms.Screen object so we don't need to
/// take a dependency on WinForms just for screen info. /// take a dependency on WinForms just for screen info.
/// </summary> /// </summary>
public sealed class ScreenInfo public sealed record ScreenInfo
{ {
public ScreenInfo(int handle, bool primary, RectangleInfo displayArea, RectangleInfo workingArea) public ScreenInfo(nint handle, bool primary, RectangleInfo displayArea, RectangleInfo? workingArea)
{ {
// this.Handle is a HMONITOR that has been cast to an int because we don't want // this.Handle is a HMONITOR that has been cast to an int because we don't want
// to expose the HMONITOR type outside the current assembly. // to expose the HMONITOR type outside the current assembly.
this.Handle = handle; this.Handle = handle;
this.Primary = primary; this.Primary = primary;
this.DisplayArea = displayArea ?? throw new ArgumentNullException(nameof(displayArea)); this.DisplayArea = displayArea ?? throw new ArgumentNullException(nameof(displayArea));
this.WorkingArea = workingArea ?? throw new ArgumentNullException(nameof(workingArea)); this.WorkingArea = workingArea;
} }
public int Handle [JsonPropertyName("handle")]
[JsonConverter(typeof(NintJsonConverter))]
public nint Handle
{ {
get; get;
} }
[JsonPropertyName("primary")]
public bool Primary public bool Primary
{ {
get; get;
} }
[JsonPropertyName("displayArea")]
public RectangleInfo DisplayArea public RectangleInfo DisplayArea
{ {
get; get;
} }
public RectangleInfo WorkingArea [JsonPropertyName("workingArea")]
public RectangleInfo? WorkingArea
{ {
get; get;
} }

View File

@@ -0,0 +1,167 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using MouseJump.Models.Styles;
namespace MouseJump.Models.Drawing;
/// <summary>
/// Represents the bounds of a layout box, including outer, margin, border, padding,
/// and content bounds. This class is used to model the box layout as described
/// in the CSS box model.
/// </summary>
public sealed class BoxBounds
{
/*
see https://www.w3schools.com/css/css_boxmodel.asp
+--------------[bounds]---------------+
|▒▒▒▒▒▒▒▒▒▒▒▒▒▒[margin]▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒|
|▒▒▓▓▓▓▓▓▓▓▓▓▓▓[border]▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒|
|▒▒▓▓░░░░░░░░░░[padding]░░░░░░░░░░▓▓▒▒|
|▒▒▓▓░░ ░░▓▓▒▒|
|▒▒▓▓░░ ░░▓▓▒▒|
|▒▒▓▓░░ [content] ░░▓▓▒▒|
|▒▒▓▓░░ ░░▓▓▒▒|
|▒▒▓▓░░ ░░▓▓▒▒|
|▒▒▓▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓▓▒▒|
|▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒|
|▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒|
+-------------------------------------+
*/
public static readonly BoxBounds Empty = new(
outerBounds: RectangleInfo.Empty,
marginBounds: RectangleInfo.Empty,
borderBounds: RectangleInfo.Empty,
paddingBounds: RectangleInfo.Empty,
contentBounds: RectangleInfo.Empty,
isEmpty: true);
public BoxBounds(
RectangleInfo outerBounds,
RectangleInfo marginBounds,
RectangleInfo borderBounds,
RectangleInfo paddingBounds,
RectangleInfo contentBounds)
: this(
outerBounds,
marginBounds,
borderBounds,
paddingBounds,
contentBounds,
isEmpty: false)
{
}
private BoxBounds(
RectangleInfo outerBounds,
RectangleInfo marginBounds,
RectangleInfo borderBounds,
RectangleInfo paddingBounds,
RectangleInfo contentBounds,
bool isEmpty)
{
this.OuterBounds = outerBounds ?? throw new ArgumentNullException(nameof(outerBounds));
this.MarginBounds = marginBounds ?? throw new ArgumentNullException(nameof(marginBounds));
this.BorderBounds = borderBounds ?? throw new ArgumentNullException(nameof(borderBounds));
this.PaddingBounds = paddingBounds ?? throw new ArgumentNullException(nameof(paddingBounds));
this.ContentBounds = contentBounds ?? throw new ArgumentNullException(nameof(contentBounds));
this.IsEmpty = isEmpty;
}
/// <summary>
/// Gets the outer bounds of this layout box.
/// </summary>
public RectangleInfo OuterBounds
{
get;
}
public RectangleInfo MarginBounds
{
get;
}
public RectangleInfo BorderBounds
{
get;
}
public RectangleInfo PaddingBounds
{
get;
}
/// <summary>
/// Gets the bounds of the content area for this layout box.
/// </summary>
public RectangleInfo ContentBounds
{
get;
}
public bool IsEmpty
{
get;
}
/// <summary>
/// Calculates the bounds of the various areas of a box, given the content bounds and the box style.
/// Starts with the content bounds and works outward, enlarging the content bounds by the padding, border, and margin sizes to calculate the outer bounds of the box.
/// </summary>
/// <param name="contentBounds">The content bounds of the box.</param>
/// <param name="boxStyle">The style of the box, which includes the sizes of the margin, border, and padding areas.</param>
/// <returns>A <see cref="BoxBounds"/> object that represents the bounds of the different areas of the box.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="contentBounds"/> or <paramref name="boxStyle"/> is null.</exception>
/// <exception cref="ArgumentException">Thrown when any of the styles in <paramref name="boxStyle"/> is null.</exception>
public static BoxBounds CreateFromContentBounds(
RectangleInfo contentBounds,
BoxStyle boxStyle)
{
ArgumentNullException.ThrowIfNull(contentBounds);
ArgumentNullException.ThrowIfNull(boxStyle);
if (boxStyle.PaddingStyle == null || boxStyle.BorderStyle == null || boxStyle.MarginStyle == null)
{
throw new ArgumentException(null, nameof(boxStyle));
}
var paddingBounds = contentBounds.Enlarge(boxStyle.PaddingStyle);
var borderBounds = paddingBounds.Enlarge(boxStyle.BorderStyle);
var marginBounds = borderBounds.Enlarge(boxStyle.MarginStyle);
var outerBounds = marginBounds;
return new(
outerBounds, marginBounds, borderBounds, paddingBounds, contentBounds);
}
/// <summary>
/// Calculates the bounds of the various areas of a box, given the outer bounds and the box style.
/// This method starts with the outer bounds and works inward, shrinking the outer bounds by the margin, border, and padding sizes to calculate the content bounds of the box.
/// </summary>
/// <param name="outerBounds">The outer bounds of the box.</param>
/// <param name="boxStyle">The style of the box, which includes the sizes of the margin, border, and padding areas.</param>
/// <returns>A <see cref="BoxBounds"/> object that represents the bounds of the different areas of the box.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="outerBounds"/> or <paramref name="boxStyle"/> is null.</exception>
/// <exception cref="ArgumentException">Thrown when any of the styles in <paramref name="boxStyle"/> is null.</exception>
public static BoxBounds CreateFromOuterBounds(
RectangleInfo outerBounds,
BoxStyle boxStyle)
{
ArgumentNullException.ThrowIfNull(outerBounds);
ArgumentNullException.ThrowIfNull(boxStyle);
if (outerBounds == null || boxStyle.MarginStyle == null || boxStyle.BorderStyle == null || boxStyle.PaddingStyle == null)
{
throw new ArgumentException(null, nameof(boxStyle));
}
var marginBounds = outerBounds;
var borderBounds = marginBounds.Shrink(boxStyle.MarginStyle);
var paddingBounds = borderBounds.Shrink(boxStyle.BorderStyle);
var contentBounds = paddingBounds.Shrink(boxStyle.PaddingStyle);
return new(
outerBounds, marginBounds, borderBounds, paddingBounds, contentBounds);
}
}

View File

@@ -2,7 +2,9 @@
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
namespace MouseJump.Common.Models.Drawing; using System.Drawing;
namespace MouseJump.Models.Drawing;
/// <summary> /// <summary>
/// Immutable version of a System.Drawing.Point object with some extra utility methods. /// Immutable version of a System.Drawing.Point object with some extra utility methods.
@@ -40,9 +42,30 @@ public sealed class PointInfo
y: Math.Clamp(this.Y, outer.Y, outer.Bottom)); y: Math.Clamp(this.Y, outer.Y, outer.Bottom));
} }
public PointInfo Offset(PointInfo amount) => new(this.X + amount.X, this.Y + amount.Y);
public PointInfo Offset(SizeInfo amount) => new(this.X + amount.Width, this.Y + amount.Height);
public PointInfo Offset(decimal amountX, decimal amountY) => new(this.X + amountX, this.Y + amountY);
public PointInfo Scale(decimal scalingFactor) => new(this.X * scalingFactor, this.Y * scalingFactor); public PointInfo Scale(decimal scalingFactor) => new(this.X * scalingFactor, this.Y * scalingFactor);
public PointInfo Offset(PointInfo amount) => new(this.X + amount.X, this.Y + amount.Y); /// <summary>
/// Stretches the point to the same proportional position in targetBounds as
/// it currently is in sourceBounds
/// </summary>
public PointInfo Stretch(RectangleInfo source, RectangleInfo target) =>
new(
x: ((this.X - source.X) / source.Width * target.Width) + target.X,
y: ((this.Y - source.Y) / source.Height * target.Height) + target.Y);
/// <summary>
/// Subtracts the specified point from the current point.
/// </summary>
public PointInfo Subtract(PointInfo point) =>
new(
x: this.X - point.X,
y: this.Y - point.Y);
public Point ToPoint() => new((int)this.X, (int)this.Y); public Point ToPoint() => new((int)this.X, (int)this.Y);
@@ -51,17 +74,6 @@ public sealed class PointInfo
return new((int)this.X, (int)this.Y); return new((int)this.X, (int)this.Y);
} }
/// <summary>
/// Stretches the point to the same proportional position in targetBounds as
/// it currently is in sourceBounds
/// </summary>
public PointInfo Stretch(RectangleInfo source, RectangleInfo target)
{
return new PointInfo(
x: ((this.X - source.X) / source.Width * target.Width) + target.X,
y: ((this.Y - source.Y) / source.Height * target.Height) + target.Y);
}
public PointInfo Truncate() => public PointInfo Truncate() =>
new( new(
(int)this.X, (int)this.X,

View File

@@ -2,12 +2,14 @@
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System.Drawing;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using MouseJump.Common.Models.Styles; using MouseJump.Models.Styles;
using BorderStyle = MouseJump.Common.Models.Styles.BorderStyle;
namespace MouseJump.Common.Models.Drawing; using BorderStyle = MouseJump.Models.Styles.BorderStyle;
namespace MouseJump.Models.Drawing;
/// <summary> /// <summary>
/// Immutable version of a System.Drawing.Rectangle object with some extra utility methods. /// Immutable version of a System.Drawing.Rectangle object with some extra utility methods.
@@ -16,6 +18,7 @@ public sealed class RectangleInfo
{ {
public static readonly RectangleInfo Empty = new(0, 0, 0, 0); public static readonly RectangleInfo Empty = new(0, 0, 0, 0);
[JsonConstructor]
public RectangleInfo(decimal x, decimal y, decimal width, decimal height) public RectangleInfo(decimal x, decimal y, decimal width, decimal height)
{ {
this.X = x; this.X = x;
@@ -29,7 +32,7 @@ public sealed class RectangleInfo
{ {
} }
public RectangleInfo(Point location, SizeInfo size) public RectangleInfo(PointInfo location, SizeInfo size)
: this(location.X, location.Y, size.Width, size.Height) : this(location.X, location.Y, size.Width, size.Height)
{ {
} }
@@ -39,26 +42,34 @@ public sealed class RectangleInfo
{ {
} }
[JsonPropertyName("x")]
public decimal X public decimal X
{ {
get; get;
} }
[JsonPropertyName("y")]
public decimal Y public decimal Y
{ {
get; get;
} }
[JsonPropertyName("width")]
public decimal Width public decimal Width
{ {
get; get;
} }
[JsonPropertyName("height")]
public decimal Height public decimal Height
{ {
get; get;
} }
[JsonIgnore]
public bool IsEmpty =>
object.ReferenceEquals(this, RectangleInfo.Empty);
[JsonIgnore] [JsonIgnore]
public decimal Left => public decimal Left =>
this.X; this.X;
@@ -83,6 +94,9 @@ public sealed class RectangleInfo
public PointInfo Location => public PointInfo Location =>
new(this.X, this.Y); new(this.X, this.Y);
/// <summary>
/// Gets the midpoint of the current rectangle.
/// </summary>
[JsonIgnore] [JsonIgnore]
public PointInfo Midpoint => public PointInfo Midpoint =>
new( new(
@@ -104,27 +118,6 @@ public sealed class RectangleInfo
width: this.Width, width: this.Width,
height: this.Height); height: this.Height);
/// <summary>
/// Returns a new <see cref="RectangleInfo"/> that is moved within the bounds of the specified outer rectangle.
/// If the current rectangle is larger than the outer rectangle, an exception is thrown.
/// </summary>
/// <param name="outer">The outer <see cref="RectangleInfo"/> within which to confine this rectangle.</param>
/// <returns>A new <see cref="RectangleInfo"/> that is the result of moving this rectangle within the bounds of the outer rectangle.</returns>
/// <exception cref="ArgumentException">Thrown when the current rectangle is larger than the outer rectangle.</exception>
public RectangleInfo Clamp(RectangleInfo outer)
{
if ((this.Width > outer.Width) || (this.Height > outer.Height))
{
throw new ArgumentException($"Value cannot be larger than {nameof(outer)}.");
}
return new(
x: Math.Clamp(this.X, outer.X, outer.Right - this.Width),
y: Math.Clamp(this.Y, outer.Y, outer.Bottom - this.Height),
width: this.Width,
height: this.Height);
}
/// <remarks> /// <remarks>
/// Adapted from https://github.com/dotnet/runtime /// Adapted from https://github.com/dotnet/runtime
/// See https://github.com/dotnet/runtime/blob/dfd618dc648ba9b11dd0f8034f78113d69f223cd/src/libraries/System.Drawing.Primitives/src/System/Drawing/Rectangle.cs /// See https://github.com/dotnet/runtime/blob/dfd618dc648ba9b11dd0f8034f78113d69f223cd/src/libraries/System.Drawing.Primitives/src/System/Drawing/Rectangle.cs
@@ -186,6 +179,53 @@ public sealed class RectangleInfo
this.Width + padding.Horizontal, this.Width + padding.Horizontal,
this.Height + padding.Vertical); this.Height + padding.Vertical);
public RectangleInfo MoveTo(PointInfo point) =>
new(
point.X,
point.Y,
this.Width,
this.Height);
/// <summary>
/// Adapted from https://github.com/dotnet/runtime
/// See https://github.com/dotnet/runtime/blob/dfd618dc648ba9b11dd0f8034f78113d69f223cd/src/libraries/System.Drawing.Primitives/src/System/Drawing/Rectangle.cs
/// </summary>
public RectangleInfo Intersect(RectangleInfo other)
{
var x1 = Math.Max(this.X, other.X);
var x2 = Math.Min(this.X + this.Width, other.X + other.Width);
var y1 = Math.Max(this.Y, other.Y);
var y2 = Math.Min(this.Y + this.Height, other.Y + other.Height);
if (x2 >= x1 && y2 >= y1)
{
return new RectangleInfo(x1, y1, x2 - x1, y2 - y1);
}
return RectangleInfo.Empty;
}
/// <summary>
/// Returns a new <see cref="RectangleInfo"/> that is moved within the bounds of the specified outer rectangle.
/// If the current rectangle is larger than the outer rectangle, an exception is thrown.
/// </summary>
/// <param name="outer">The outer <see cref="RectangleInfo"/> within which to confine this rectangle.</param>
/// <returns>A new <see cref="RectangleInfo"/> that is the result of moving this rectangle within the bounds of the outer rectangle.</returns>
/// <exception cref="ArgumentException">Thrown when the current rectangle is larger than the outer rectangle.</exception>
public RectangleInfo MoveInside(RectangleInfo outer)
{
if ((this.Width > outer.Width) || (this.Height > outer.Height))
{
throw new ArgumentException($"Value cannot be larger than {nameof(outer)}.");
}
return new(
x: Math.Clamp(this.X, outer.X, outer.Right - this.Width),
y: Math.Clamp(this.Y, outer.Y, outer.Bottom - this.Height),
width: this.Width,
height: this.Height);
}
/// <summary> /// <summary>
/// Returns a new <see cref="RectangleInfo"/> that is offset by the specified amount. /// Returns a new <see cref="RectangleInfo"/> that is offset by the specified amount.
/// </summary> /// </summary>
@@ -203,6 +243,9 @@ public sealed class RectangleInfo
public RectangleInfo Offset(decimal dx, decimal dy) => public RectangleInfo Offset(decimal dx, decimal dy) =>
new(this.X + dx, this.Y + dy, this.Width, this.Height); new(this.X + dx, this.Y + dy, this.Width, this.Height);
public RectangleInfo Resize(decimal width, decimal height) =>
new(this.X, this.Y, width, height);
public RectangleInfo Round() => public RectangleInfo Round() =>
this.Round(0); this.Round(0);
@@ -289,6 +332,16 @@ public sealed class RectangleInfo
return new RectangleInfo(x1, y1, x2 - x1, y2 - y1); return new RectangleInfo(x1, y1, x2 - x1, y2 - y1);
} }
public static RectangleInfo Union(IEnumerable<RectangleInfo> rects)
{
var list = rects.ToList();
return (list.Count == 0)
? RectangleInfo.Empty
: list.Skip(1).Aggregate(
seed: list.First(),
(aggr, rect) => aggr.Union(rect));
}
public Rectangle ToRectangle() => public Rectangle ToRectangle() =>
new( new(
(int)this.X, (int)this.X,

View File

@@ -2,16 +2,23 @@
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using MouseJump.Common.Models.Styles; using System.Drawing;
using BorderStyle = MouseJump.Common.Models.Styles.BorderStyle; using System.Text.Json.Serialization;
namespace MouseJump.Common.Models.Drawing; using MouseJump.Models.Styles;
using BorderStyle = MouseJump.Models.Styles.BorderStyle;
namespace MouseJump.Models.Drawing;
/// <summary> /// <summary>
/// Immutable version of a System.Drawing.Size object with some extra utility methods. /// Immutable version of a System.Drawing.Size object with some extra utility methods.
/// </summary> /// </summary>
public sealed class SizeInfo public sealed class SizeInfo
{ {
public static readonly SizeInfo Empty = new(0, 0);
[JsonConstructor]
public SizeInfo(decimal width, decimal height) public SizeInfo(decimal width, decimal height)
{ {
this.Width = width; this.Width = width;
@@ -33,6 +40,9 @@ public sealed class SizeInfo
get; get;
} }
public bool IsEmpty =>
object.ReferenceEquals(this, SizeInfo.Empty);
public SizeInfo Clamp(SizeInfo max) public SizeInfo Clamp(SizeInfo max)
{ {
return new( return new(
@@ -40,13 +50,6 @@ public sealed class SizeInfo
height: Math.Clamp(this.Height, 0, max.Height)); height: Math.Clamp(this.Height, 0, max.Height));
} }
public SizeInfo Clamp(decimal maxWidth, decimal maxHeight)
{
return new(
width: Math.Clamp(this.Width, 0, maxWidth),
height: Math.Clamp(this.Height, 0, maxHeight));
}
public SizeInfo Enlarge(BorderStyle border) => public SizeInfo Enlarge(BorderStyle border) =>
new( new(
this.Width + border.Horizontal, this.Width + border.Horizontal,
@@ -101,7 +104,7 @@ public sealed class SizeInfo
/// <param name="x">The x-coordinate of the upper-left corner of the rectangle.</param> /// <param name="x">The x-coordinate of the upper-left corner of the rectangle.</param>
/// <param name="y">The y-coordinate of the upper-left corner of the rectangle.</param> /// <param name="y">The y-coordinate of the upper-left corner of the rectangle.</param>
/// <returns>A new <see cref="RectangleInfo"/> instance representing the positioned rectangle.</returns> /// <returns>A new <see cref="RectangleInfo"/> instance representing the positioned rectangle.</returns>
public RectangleInfo PlaceAt(decimal x, decimal y) => public RectangleInfo MoveTo(decimal x, decimal y) =>
new(x, y, this.Width, this.Height); new(x, y, this.Width, this.Height);
public SizeInfo Round() => public SizeInfo Round() =>
@@ -120,21 +123,31 @@ public sealed class SizeInfo
/// </summary> /// </summary>
/// <param name="bounds">The size to fit this size into.</param> /// <param name="bounds">The size to fit this size into.</param>
/// <returns>A new <see cref="SizeInfo"/> instance representing the scaled size.</returns> /// <returns>A new <see cref="SizeInfo"/> instance representing the scaled size.</returns>
public SizeInfo ScaleToFit(SizeInfo bounds, out decimal scalingRatio) public SizeInfo ScaleToFit(SizeInfo bounds)
{
return this.ScaleToFit(bounds, out var _);
}
/// <summary>
/// Scales this size to fit within the bounds of another size, while maintaining the aspect ratio.
/// </summary>
/// <param name="bounds">The size to fit this size into.</param>
/// <returns>A new <see cref="SizeInfo"/> instance representing the scaled size.</returns>
public SizeInfo ScaleToFit(SizeInfo bounds, out decimal scalingFactor)
{ {
var widthRatio = bounds.Width / this.Width; var widthRatio = bounds.Width / this.Width;
var heightRatio = bounds.Height / this.Height; var heightRatio = bounds.Height / this.Height;
switch (widthRatio.CompareTo(heightRatio)) switch (widthRatio.CompareTo(heightRatio))
{ {
case < 0: case < 0:
scalingRatio = widthRatio; scalingFactor = widthRatio;
return new(bounds.Width, this.Height * widthRatio); return new(bounds.Width, this.Height * widthRatio);
case 0: case 0:
// widthRatio and heightRatio are the same, so just pick one // widthRatio and heightRatio are the same, so just pick one
scalingRatio = widthRatio; scalingFactor = widthRatio;
return bounds; return bounds;
case > 0: case > 0:
scalingRatio = heightRatio; scalingFactor = heightRatio;
return new(this.Width * heightRatio, bounds.Height); return new(this.Width * heightRatio, bounds.Height);
} }
} }

View File

@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="$(RepoRoot)src\Common.Dotnet.CsWinRT.props" />
<PropertyGroup>
<ProjectGuid>{478C3576-CF1C-4509-B793-AE10DF394347}</ProjectGuid>
<AssemblyName>PowerToys.MouseJump.Models</AssemblyName>
<AssemblyTitle>PowerToys.MouseJump.Models</AssemblyTitle>
<AssemblyDescription>PowerToys MouseJump.Models</AssemblyDescription>
<OutputType>Library</OutputType>
<OutputPath>$(RepoRoot)$(Platform)\$(Configuration)\WinUI3Apps</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<ApplicationHighDpiMode>PerMonitorV2</ApplicationHighDpiMode>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,23 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Text.Json;
using System.Text.Json.Serialization;
namespace MouseJump.Models;
public sealed class NintJsonConverter : JsonConverter<nint>
{
public override nint Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
// nint can be 32 or 64 bit, so convert to Int64
var value = reader.GetInt64();
return (nint)value;
}
public override void Write(Utf8JsonWriter writer, nint value, JsonSerializerOptions options)
{
writer.WriteNumberValue((long)value);
}
}

View File

@@ -2,7 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
namespace MouseJump.Common.Models.Settings; namespace MouseJump.Models.Settings;
public enum PreviewType public enum PreviewType
{ {

View File

@@ -2,7 +2,9 @@
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
namespace MouseJump.Common.Models.Styles; using System.Drawing;
namespace MouseJump.Models.Styles;
/// <summary> /// <summary>
/// Represents the background fill style for a drawing object. /// Represents the background fill style for a drawing object.

View File

@@ -2,7 +2,9 @@
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
namespace MouseJump.Common.Models.Styles; using System.Drawing;
namespace MouseJump.Models.Styles;
/// <summary> /// <summary>
/// Represents the border style for a drawing object. /// Represents the border style for a drawing object.
@@ -63,6 +65,11 @@ public sealed class BorderStyle
public decimal Vertical => this.Top + this.Bottom; public decimal Vertical => this.Top + this.Bottom;
public BorderStyle WithColor(Color? color)
{
return new BorderStyle(color, this.Left, this.Top, this.Right, this.Bottom, this.Depth);
}
public override string ToString() public override string ToString()
{ {
return "{" + return "{" +

View File

@@ -2,7 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
namespace MouseJump.Common.Models.Styles; namespace MouseJump.Models.Styles;
/// <summary> /// <summary>
/// Represents the styles to apply to a simple box-layout based drawing object. /// Represents the styles to apply to a simple box-layout based drawing object.
@@ -29,18 +29,34 @@ public sealed class BoxStyle
*/ */
public static readonly BoxStyle Empty = new(MarginStyle.Empty, BorderStyle.Empty, PaddingStyle.Empty, BackgroundStyle.Empty); public static readonly BoxStyle Empty = new(
MarginStyle.Empty,
BorderStyle.Empty,
PaddingStyle.Empty,
BackgroundStyle.Empty,
isEmpty: true);
public BoxStyle( public BoxStyle(
MarginStyle marginStyle, MarginStyle marginStyle,
BorderStyle borderStyle, BorderStyle borderStyle,
PaddingStyle paddingStyle, PaddingStyle paddingStyle,
BackgroundStyle backgroundStyle) BackgroundStyle backgroundStyle)
: this(marginStyle, borderStyle, paddingStyle, backgroundStyle, false)
{
}
private BoxStyle(
MarginStyle marginStyle,
BorderStyle borderStyle,
PaddingStyle paddingStyle,
BackgroundStyle backgroundStyle,
bool isEmpty)
{ {
this.MarginStyle = marginStyle ?? throw new ArgumentNullException(nameof(marginStyle)); this.MarginStyle = marginStyle ?? throw new ArgumentNullException(nameof(marginStyle));
this.BorderStyle = borderStyle ?? throw new ArgumentNullException(nameof(borderStyle)); this.BorderStyle = borderStyle ?? throw new ArgumentNullException(nameof(borderStyle));
this.PaddingStyle = paddingStyle ?? throw new ArgumentNullException(nameof(paddingStyle)); this.PaddingStyle = paddingStyle ?? throw new ArgumentNullException(nameof(paddingStyle));
this.BackgroundStyle = backgroundStyle ?? throw new ArgumentNullException(nameof(backgroundStyle)); this.BackgroundStyle = backgroundStyle ?? throw new ArgumentNullException(nameof(backgroundStyle));
this.IsEmpty = isEmpty;
} }
/// <summary> /// <summary>
@@ -74,4 +90,9 @@ public sealed class BoxStyle
{ {
get; get;
} }
public bool IsEmpty
{
get;
}
} }

View File

@@ -2,7 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
namespace MouseJump.Common.Models.Styles; namespace MouseJump.Models.Styles;
/// <summary> /// <summary>
/// Represents the margin style for a drawing object. /// Represents the margin style for a drawing object.

View File

@@ -2,7 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
namespace MouseJump.Common.Models.Styles; namespace MouseJump.Models.Styles;
/// <summary> /// <summary>
/// Represents the margin style for a drawing object. /// Represents the margin style for a drawing object.

View File

@@ -2,20 +2,25 @@
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using MouseJump.Common.Models.Drawing; using System.Collections.ObjectModel;
using System.Drawing;
namespace MouseJump.Common.Models.Styles; using MouseJump.Models.Drawing;
namespace MouseJump.Models.Styles;
public sealed class PreviewStyle public sealed class PreviewStyle
{ {
public PreviewStyle( public PreviewStyle(
SizeInfo canvasSize, SizeInfo canvasSize,
BoxStyle canvasStyle, BoxStyle canvasStyle,
BoxStyle screenStyle) BoxStyle screenStyle,
IEnumerable<Color> extraColors)
{ {
this.CanvasSize = canvasSize ?? throw new ArgumentNullException(nameof(canvasSize)); this.CanvasSize = canvasSize ?? throw new ArgumentNullException(nameof(canvasSize));
this.CanvasStyle = canvasStyle ?? throw new ArgumentNullException(nameof(canvasStyle)); this.CanvasStyle = canvasStyle ?? throw new ArgumentNullException(nameof(canvasStyle));
this.ScreenStyle = screenStyle ?? throw new ArgumentNullException(nameof(screenStyle)); this.ScreenStyle = screenStyle ?? throw new ArgumentNullException(nameof(screenStyle));
this.ExtraColors = (extraColors ?? throw new ArgumentNullException(nameof(extraColors))).ToList().AsReadOnly();
} }
public SizeInfo CanvasSize public SizeInfo CanvasSize
@@ -32,4 +37,9 @@ public sealed class PreviewStyle
{ {
get; get;
} }
public ReadOnlyCollection<Color> ExtraColors
{
get;
}
} }

Some files were not shown because too many files have changed in this diff Show More