fix: gate Windows cursor settings

This commit is contained in:
EtienneLescot
2026-05-05 19:24:32 +02:00
parent 38d727eb8e
commit c0deb03414
5 changed files with 192 additions and 5 deletions
@@ -111,6 +111,62 @@ function Write-JsonLine($payload) {
[Console]::Out.WriteLine(($payload | ConvertTo-Json -Compress -Depth 6))
}
function Get-CustomCursorType($bitmap, $hotspotX, $hotspotY) {
if ($bitmap.Width -lt 24 -or $bitmap.Height -lt 24 -or $bitmap.Width -gt 64 -or $bitmap.Height -gt 64) {
return $null
}
if ($hotspotX -lt ($bitmap.Width * 0.25) -or $hotspotX -gt ($bitmap.Width * 0.75) -or
$hotspotY -lt ($bitmap.Height * 0.15) -or $hotspotY -gt ($bitmap.Height * 0.55)) {
return $null
}
$opaquePixels = 0
$topHalfOpaquePixels = 0
$left = $bitmap.Width
$top = $bitmap.Height
$right = -1
$bottom = -1
for ($y = 0; $y -lt $bitmap.Height; $y++) {
for ($x = 0; $x -lt $bitmap.Width; $x++) {
if ($bitmap.GetPixel($x, $y).A -le 32) {
continue
}
$opaquePixels += 1
if ($y -lt ($bitmap.Height / 2)) {
$topHalfOpaquePixels += 1
}
if ($x -lt $left) { $left = $x }
if ($x -gt $right) { $right = $x }
if ($y -lt $top) { $top = $y }
if ($y -gt $bottom) { $bottom = $y }
}
}
if ($opaquePixels -lt 90 -or $right -lt $left -or $bottom -lt $top) {
return $null
}
$opaqueWidth = $right - $left + 1
$opaqueHeight = $bottom - $top + 1
if ($opaqueWidth -lt ($bitmap.Width * 0.35) -or $opaqueWidth -gt ($bitmap.Width * 0.9) -or
$opaqueHeight -lt ($bitmap.Height * 0.45) -or $opaqueHeight -gt $bitmap.Height) {
return $null
}
if ($top -gt ($bitmap.Height * 0.45) -or $bottom -lt ($bitmap.Height * 0.65)) {
return $null
}
if ($topHalfOpaquePixels -gt ($opaquePixels * 0.55)) {
return 'closed-hand'
}
return 'open-hand'
}
function Get-TargetBounds() {
if ([string]::IsNullOrWhiteSpace($targetWindowHandle)) {
return $null
@@ -164,6 +220,9 @@ function Get-CursorAsset($cursorHandle, $cursorId) {
try {
$graphics.Clear([System.Drawing.Color]::Transparent)
$graphics.DrawIcon($icon, 0, 0)
$hotspotX = if ($hasIconInfo) { $iconInfo.xHotspot } else { 0 }
$hotspotY = if ($hasIconInfo) { $iconInfo.yHotspot } else { 0 }
$customCursorType = Get-CustomCursorType -bitmap $bitmap -hotspotX $hotspotX -hotspotY $hotspotY
$bitmap.Save($memoryStream, [System.Drawing.Imaging.ImageFormat]::Png)
$base64 = [System.Convert]::ToBase64String($memoryStream.ToArray())
@@ -172,8 +231,9 @@ function Get-CursorAsset($cursorHandle, $cursorId) {
imageDataUrl = "data:image/png;base64,$base64"
width = $bitmap.Width
height = $bitmap.Height
hotspotX = if ($hasIconInfo) { $iconInfo.xHotspot } else { 0 }
hotspotY = if ($hasIconInfo) { $iconInfo.yHotspot } else { 0 }
hotspotX = $hotspotX
hotspotY = $hotspotY
cursorType = $customCursorType
}
}
finally {
@@ -218,6 +278,8 @@ while ($true) {
$asset = Get-CursorAsset -cursorHandle $cursorInfo.hCursor -cursorId $cursorId
if ($asset -and $cursorType) {
$asset.cursorType = $cursorType
} elseif ($asset -and $asset.cursorType) {
$cursorType = $asset.cursorType
}
$lastCursorId = $cursorId
}
+65 -2
View File
@@ -195,6 +195,62 @@ function Write-JsonLine($payload) {
[Console]::Out.WriteLine(($payload | ConvertTo-Json -Compress -Depth 6))
}
function Get-CustomCursorType($bitmap, $hotspotX, $hotspotY) {
if ($bitmap.Width -lt 24 -or $bitmap.Height -lt 24 -or $bitmap.Width -gt 64 -or $bitmap.Height -gt 64) {
return $null
}
if ($hotspotX -lt ($bitmap.Width * 0.25) -or $hotspotX -gt ($bitmap.Width * 0.75) -or
$hotspotY -lt ($bitmap.Height * 0.15) -or $hotspotY -gt ($bitmap.Height * 0.55)) {
return $null
}
$opaquePixels = 0
$topHalfOpaquePixels = 0
$left = $bitmap.Width
$top = $bitmap.Height
$right = -1
$bottom = -1
for ($y = 0; $y -lt $bitmap.Height; $y++) {
for ($x = 0; $x -lt $bitmap.Width; $x++) {
if ($bitmap.GetPixel($x, $y).A -le 32) {
continue
}
$opaquePixels += 1
if ($y -lt ($bitmap.Height / 2)) {
$topHalfOpaquePixels += 1
}
if ($x -lt $left) { $left = $x }
if ($x -gt $right) { $right = $x }
if ($y -lt $top) { $top = $y }
if ($y -gt $bottom) { $bottom = $y }
}
}
if ($opaquePixels -lt 90 -or $right -lt $left -or $bottom -lt $top) {
return $null
}
$opaqueWidth = $right - $left + 1
$opaqueHeight = $bottom - $top + 1
if ($opaqueWidth -lt ($bitmap.Width * 0.35) -or $opaqueWidth -gt ($bitmap.Width * 0.9) -or
$opaqueHeight -lt ($bitmap.Height * 0.45) -or $opaqueHeight -gt $bitmap.Height) {
return $null
}
if ($top -gt ($bitmap.Height * 0.45) -or $bottom -lt ($bitmap.Height * 0.65)) {
return $null
}
if ($topHalfOpaquePixels -gt ($opaquePixels * 0.55)) {
return 'closed-hand'
}
return 'open-hand'
}
function Get-CursorAsset($cursorHandle, $cursorId) {
$copiedHandle = [OpenScreenCursorDiagnosticInterop]::CopyIcon($cursorHandle)
if ($copiedHandle -eq [IntPtr]::Zero) {
@@ -213,6 +269,9 @@ function Get-CursorAsset($cursorHandle, $cursorId) {
try {
$graphics.Clear([System.Drawing.Color]::Transparent)
$graphics.DrawIcon($icon, 0, 0)
$hotspotX = if ($hasIconInfo) { $iconInfo.xHotspot } else { 0 }
$hotspotY = if ($hasIconInfo) { $iconInfo.yHotspot } else { 0 }
$customCursorType = Get-CustomCursorType -bitmap $bitmap -hotspotX $hotspotX -hotspotY $hotspotY
$bitmap.Save($memoryStream, [System.Drawing.Imaging.ImageFormat]::Png)
$base64 = [System.Convert]::ToBase64String($memoryStream.ToArray())
@@ -221,8 +280,9 @@ function Get-CursorAsset($cursorHandle, $cursorId) {
imageDataUrl = "data:image/png;base64,$base64"
width = $bitmap.Width
height = $bitmap.Height
hotspotX = if ($hasIconInfo) { $iconInfo.xHotspot } else { 0 }
hotspotY = if ($hasIconInfo) { $iconInfo.yHotspot } else { 0 }
hotspotX = $hotspotX
hotspotY = $hotspotY
cursorType = $customCursorType
}
}
finally {
@@ -268,6 +328,8 @@ while ($true) {
$asset = Get-CursorAsset -cursorHandle $cursorInfo.hCursor -cursorId $cursorId
if ($asset -and $cursorType) {
$asset.cursorType = $cursorType
} elseif ($asset -and $asset.cursorType) {
$cursorType = $asset.cursorType
}
$lastCursorId = $cursorId
}
@@ -1068,6 +1130,7 @@ const report = {
height: asset.height,
hotspotX: asset.hotspotX,
hotspotY: asset.hotspotY,
cursorType: asset.cursorType ?? null,
})),
};
const recordingData = toRecordingData(samples, assets);
@@ -39,6 +39,7 @@ import {
} from "@/lib/userPreferences";
import { BackgroundLoadError } from "@/lib/wallpaper";
import { nativeBridgeClient, useCursorRecordingData, useCursorTelemetry } from "@/native";
import type { NativePlatform } from "@/native/contracts";
import {
getAspectRatioValue,
getNativeAspectRatioValue,
@@ -164,6 +165,7 @@ export default function VideoEditor() {
const [cursorSmoothing, setCursorSmoothing] = useState(DEFAULT_CURSOR_SMOOTHING);
const [cursorMotionBlur, setCursorMotionBlur] = useState(DEFAULT_CURSOR_MOTION_BLUR);
const [cursorClickBounce, setCursorClickBounce] = useState(DEFAULT_CURSOR_CLICK_BOUNCE);
const [nativePlatform, setNativePlatform] = useState<NativePlatform | null>(null);
const videoPlaybackRef = useRef<VideoPlaybackRef>(null);
@@ -172,6 +174,7 @@ export default function VideoEditor() {
const nextSpeedIdRef = useRef(1);
const { shortcuts, isMac } = useShortcuts();
const showCursorSettings = nativePlatform === "win32";
// Off-Mac doesn't have click telemetry, so force `onlyOnClicks` off for
// renderers while keeping the persisted value intact for round-tripping.
const effectiveCursorHighlight = useMemo(
@@ -631,6 +634,27 @@ export default function VideoEditor() {
};
}, [handleLoadProject, handleSaveProject, handleSaveProjectAs]);
useEffect(() => {
let canceled = false;
nativeBridgeClient.system
.getPlatform()
.then((platform) => {
if (!canceled) {
setNativePlatform(platform);
}
})
.catch((error) => {
console.warn("Unable to resolve native platform for cursor settings:", error);
if (!canceled) {
setNativePlatform(null);
}
});
return () => {
canceled = true;
};
}, []);
useEffect(() => {
if (cursorTelemetryError) {
console.warn("Unable to load cursor telemetry:", cursorTelemetryError);
@@ -1718,6 +1742,8 @@ export default function VideoEditor() {
cursorTelemetry,
cursorClickTimestamps,
effectiveCursorHighlight,
showCursor,
cursorSize,
t,
],
);
+35 -1
View File
@@ -2,6 +2,8 @@ import { type Container, Point } from "pixi.js";
import appStartingUrl from "@/assets/cursors/Cursor=App-Starting.svg";
import crosshairUrl from "@/assets/cursors/Cursor=Cross.svg";
import arrowUrl from "@/assets/cursors/Cursor=Default.svg";
import closedHandUrl from "@/assets/cursors/Cursor=Hand-(Grabbing).svg";
import openHandUrl from "@/assets/cursors/Cursor=Hand-(Open).svg";
import pointerUrl from "@/assets/cursors/Cursor=Hand-(Pointing).svg";
import helpUrl from "@/assets/cursors/Cursor=Help.svg";
import moveUrl from "@/assets/cursors/Cursor=Move.svg";
@@ -78,6 +80,20 @@ const PRETTY_NATIVE_CURSOR_ASSETS: Partial<Record<NativeCursorType, PrettyNative
hotspotX: 16,
hotspotY: 16,
},
"open-hand": {
imageDataUrl: openHandUrl,
width: 32,
height: 32,
hotspotX: 16,
hotspotY: 9,
},
"closed-hand": {
imageDataUrl: closedHandUrl,
width: 32,
height: 32,
hotspotX: 16,
hotspotY: 9,
},
"resize-ew": {
imageDataUrl: resizeEwUrl,
width: 32,
@@ -150,6 +166,22 @@ const PRETTY_NATIVE_CURSOR_ASSETS: Partial<Record<NativeCursorType, PrettyNative
},
};
function resolveUntypedPrettyNativeCursorAsset(asset: NativeCursorAsset) {
if (asset.cursorType || asset.width < 24 || asset.width > 64 || asset.height < 24 || asset.height > 64) {
return null;
}
const hotspotXNorm = asset.hotspotX / asset.width;
const hotspotYNorm = asset.hotspotY / asset.height;
const looksLikeChromiumGrabCursor =
hotspotXNorm >= 0.22 &&
hotspotXNorm <= 0.55 &&
hotspotYNorm >= 0.2 &&
hotspotYNorm <= 0.45;
return looksLikeChromiumGrabCursor ? (PRETTY_NATIVE_CURSOR_ASSETS["open-hand"] ?? null) : null;
}
export function hasNativeCursorRecordingData(
recordingData: CursorRecordingData | null | undefined,
): recordingData is CursorRecordingData {
@@ -322,7 +354,9 @@ export function resolvePrettyNativeCursorAsset(
sample?: CursorRecordingSample,
) {
const cursorType = sample?.cursorType ?? asset.cursorType ?? null;
return cursorType ? (PRETTY_NATIVE_CURSOR_ASSETS[cursorType] ?? null) : null;
return cursorType
? (PRETTY_NATIVE_CURSOR_ASSETS[cursorType] ?? null)
: resolveUntypedPrettyNativeCursorAsset(asset);
}
export function resolveNativeCursorRenderAsset(
+2
View File
@@ -8,6 +8,8 @@ export type NativeCursorType =
| "text"
| "pointer"
| "crosshair"
| "open-hand"
| "closed-hand"
| "resize-ew"
| "resize-ns"
| "resize-nesw"