mirror of
https://github.com/siddharthvaddem/openscreen.git
synced 2026-09-01 15:35:00 +08:00
feat: capture native cursor bitmaps on macOS
Capture the real system cursor image during macOS recording so custom and default cursors render natively instead of being mapped to bundled SVGs, bringing macOS in line with the Windows WGC capture path. - macOS cursor helper grabs NSCursor.currentSystem as a PNG asset (SHA256 id, intrinsic scale factor, pixel hotspot); the bitmap payload is emitted once per shape and referenced by assetId thereafter - helper returns nil cursorType instead of an arrow fallback so default/custom cursors fall through to the captured bitmap while text/pointer stay beautified - MacNativeCursorRecordingSession collects deduped assets, tags samples with assetId, and reports provider "native" when bitmaps are captured
This commit is contained in:
@@ -6,10 +6,21 @@ import { type Rectangle, screen, systemPreferences } from "electron";
|
||||
import type {
|
||||
CursorRecordingData,
|
||||
CursorRecordingSample,
|
||||
NativeCursorAsset,
|
||||
NativeCursorType,
|
||||
} from "../../../../src/native/contracts";
|
||||
import type { CursorRecordingSession } from "./session";
|
||||
|
||||
interface MacCursorAssetPayload {
|
||||
id: string;
|
||||
imageDataUrl: string;
|
||||
width: number;
|
||||
height: number;
|
||||
hotspotX: number;
|
||||
hotspotY: number;
|
||||
scaleFactor?: number;
|
||||
}
|
||||
|
||||
interface MacNativeCursorRecordingSessionOptions {
|
||||
getDisplayBounds: () => Rectangle | null;
|
||||
maxSamples: number;
|
||||
@@ -28,6 +39,8 @@ type MacCursorEvent =
|
||||
type: "sample";
|
||||
timestampMs: number;
|
||||
cursorType?: NativeCursorType | null;
|
||||
assetId?: string | null;
|
||||
asset?: MacCursorAssetPayload | null;
|
||||
leftButtonDown?: boolean;
|
||||
leftButtonPressed?: boolean;
|
||||
leftButtonReleased?: boolean;
|
||||
@@ -170,6 +183,7 @@ function normalizeCursorType(value: unknown): NativeCursorType | null {
|
||||
|
||||
export class MacNativeCursorRecordingSession implements CursorRecordingSession {
|
||||
private samples: CursorRecordingSample[] = [];
|
||||
private assets = new Map<string, NativeCursorAsset>();
|
||||
private process: ChildProcessByStdio<null, Readable, Readable> | null = null;
|
||||
private lineBuffer = "";
|
||||
private startTimeMs = 0;
|
||||
@@ -187,6 +201,7 @@ export class MacNativeCursorRecordingSession implements CursorRecordingSession {
|
||||
|
||||
async start(): Promise<void> {
|
||||
this.samples = [];
|
||||
this.assets.clear();
|
||||
this.lineBuffer = "";
|
||||
this.startTimeMs = this.options.startTimeMs ?? Date.now();
|
||||
this.previousLeftButtonDown = false;
|
||||
@@ -263,19 +278,38 @@ export class MacNativeCursorRecordingSession implements CursorRecordingSession {
|
||||
|
||||
return {
|
||||
version: 2,
|
||||
provider: "none",
|
||||
provider: this.assets.size > 0 ? "native" : "none",
|
||||
samples: this.samples,
|
||||
assets: [],
|
||||
assets: [...this.assets.values()],
|
||||
};
|
||||
}
|
||||
|
||||
private startPositionOnlyFallback() {
|
||||
this.captureSample(Date.now(), null, false, false, false);
|
||||
this.captureSample(Date.now(), null, null, false, false, false);
|
||||
this.fallbackInterval = setInterval(() => {
|
||||
this.captureSample(Date.now(), null, false, false, false);
|
||||
this.captureSample(Date.now(), null, null, false, false, false);
|
||||
}, this.options.sampleIntervalMs);
|
||||
}
|
||||
|
||||
private rememberAsset(asset: MacCursorAssetPayload | null | undefined) {
|
||||
if (!asset?.id || this.assets.has(asset.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cursor = screen.getCursorScreenPoint();
|
||||
const displayScaleFactor = screen.getDisplayNearestPoint(cursor).scaleFactor;
|
||||
this.assets.set(asset.id, {
|
||||
id: asset.id,
|
||||
platform: "darwin",
|
||||
imageDataUrl: asset.imageDataUrl,
|
||||
width: asset.width,
|
||||
height: asset.height,
|
||||
hotspotX: asset.hotspotX,
|
||||
hotspotY: asset.hotspotY,
|
||||
scaleFactor: asset.scaleFactor ?? displayScaleFactor,
|
||||
});
|
||||
}
|
||||
|
||||
private handleStdoutChunk(chunk: string) {
|
||||
this.lineBuffer += chunk;
|
||||
const lines = this.lineBuffer.split(/\r?\n/);
|
||||
@@ -307,9 +341,11 @@ export class MacNativeCursorRecordingSession implements CursorRecordingSession {
|
||||
}
|
||||
|
||||
if (payload.type === "sample") {
|
||||
this.rememberAsset(payload.asset);
|
||||
this.captureSample(
|
||||
payload.timestampMs,
|
||||
normalizeCursorType(payload.cursorType),
|
||||
payload.assetId ?? null,
|
||||
payload.leftButtonDown === true,
|
||||
payload.leftButtonPressed === true,
|
||||
payload.leftButtonReleased === true,
|
||||
@@ -320,6 +356,7 @@ export class MacNativeCursorRecordingSession implements CursorRecordingSession {
|
||||
private captureSample(
|
||||
timestampMs: number,
|
||||
cursorType: NativeCursorType | null,
|
||||
assetId: string | null,
|
||||
leftButtonDown: boolean,
|
||||
leftButtonPressed: boolean,
|
||||
leftButtonReleased: boolean,
|
||||
@@ -357,6 +394,7 @@ export class MacNativeCursorRecordingSession implements CursorRecordingSession {
|
||||
cy: clamp(normalizedY, 0, 1),
|
||||
visible,
|
||||
interactionType,
|
||||
...(assetId ? { assetId } : {}),
|
||||
...(cursorType ? { cursorType } : {}),
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user