Skip to content

Commit ac887dd

Browse files
committed
feat: add windows native cursor capture and rendering
1 parent ceaaa65 commit ac887dd

13 files changed

Lines changed: 1148 additions & 392 deletions

File tree

electron/ipc/handlers.ts

Lines changed: 290 additions & 303 deletions
Large diffs are not rendered by default.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import type { Rectangle } from "electron";
2+
import type { CursorRecordingSession } from "./session";
3+
import { TelemetryRecordingSession } from "./telemetryRecordingSession";
4+
import { WindowsNativeRecordingSession } from "./windowsNativeRecordingSession";
5+
6+
interface CreateCursorRecordingSessionOptions {
7+
getDisplayBounds: () => Rectangle | null;
8+
maxSamples: number;
9+
platform: NodeJS.Platform;
10+
sampleIntervalMs: number;
11+
}
12+
13+
export function createCursorRecordingSession(
14+
options: CreateCursorRecordingSessionOptions,
15+
): CursorRecordingSession {
16+
if (options.platform === "win32") {
17+
return new WindowsNativeRecordingSession({
18+
getDisplayBounds: options.getDisplayBounds,
19+
maxSamples: options.maxSamples,
20+
sampleIntervalMs: options.sampleIntervalMs,
21+
});
22+
}
23+
24+
return new TelemetryRecordingSession({
25+
getDisplayBounds: options.getDisplayBounds,
26+
maxSamples: options.maxSamples,
27+
sampleIntervalMs: options.sampleIntervalMs,
28+
});
29+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import type { CursorRecordingData } from "../../../../src/native/contracts";
2+
3+
export interface CursorRecordingSession {
4+
start(): Promise<void>;
5+
stop(): Promise<CursorRecordingData>;
6+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { type Rectangle, screen } from "electron";
2+
import type { CursorRecordingData, CursorRecordingSample } from "../../../../src/native/contracts";
3+
import type { CursorRecordingSession } from "./session";
4+
5+
interface TelemetryRecordingSessionOptions {
6+
getDisplayBounds: () => Rectangle | null;
7+
maxSamples: number;
8+
sampleIntervalMs: number;
9+
}
10+
11+
function clamp(value: number, min: number, max: number) {
12+
return Math.min(max, Math.max(min, value));
13+
}
14+
15+
export class TelemetryRecordingSession implements CursorRecordingSession {
16+
private samples: CursorRecordingSample[] = [];
17+
private interval: NodeJS.Timeout | null = null;
18+
private startTimeMs = 0;
19+
20+
constructor(private readonly options: TelemetryRecordingSessionOptions) {}
21+
22+
async start(): Promise<void> {
23+
this.samples = [];
24+
this.startTimeMs = Date.now();
25+
this.captureSample();
26+
this.interval = setInterval(() => {
27+
this.captureSample();
28+
}, this.options.sampleIntervalMs);
29+
}
30+
31+
async stop(): Promise<CursorRecordingData> {
32+
if (this.interval) {
33+
clearInterval(this.interval);
34+
this.interval = null;
35+
}
36+
37+
return {
38+
version: 2,
39+
provider: "none",
40+
samples: this.samples,
41+
assets: [],
42+
};
43+
}
44+
45+
private captureSample() {
46+
const cursor = screen.getCursorScreenPoint();
47+
const display = this.options.getDisplayBounds() ?? screen.getDisplayNearestPoint(cursor).bounds;
48+
const width = Math.max(1, display.width);
49+
const height = Math.max(1, display.height);
50+
51+
this.samples.push({
52+
timeMs: Math.max(0, Date.now() - this.startTimeMs),
53+
cx: clamp((cursor.x - display.x) / width, 0, 1),
54+
cy: clamp((cursor.y - display.y) / height, 0, 1),
55+
visible: true,
56+
});
57+
58+
if (this.samples.length > this.options.maxSamples) {
59+
this.samples.shift();
60+
}
61+
}
62+
}

0 commit comments

Comments
 (0)