Files
Recordly/electron/ipc/sourceArea.ts
KevinandClaude Opus 5.5 f91d1a1288 Pick a window, a screen or an area on screen
"Select area or window" in the source menu opens a picker over every
display. Hovering highlights the window under the cursor, or the whole
screen over the desktop, and a click picks it. Dragging draws an area,
and moving or resizing any pick turns it into an area. Record, Enter or
a double-click starts recording, Select only picks, Escape cancels.

The window-list helper gains a --z-order mode that lists windows front
to back with their owner process, so the picker hits the frontmost
window and skips Recordly's own. A picked window or screen becomes the
same source the lists offer. An area becomes an area:<displayId> source
that the capture helper crops like a window that never moves, cursor
telemetry normalizes to, and a dashed outline marks while recording.
The main process validates every pick: a window must be one the picker
offered. Picker windows stay hidden until they have painted, so opening
the picker does not flash the app background.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 11:14:23 -07:00

108 lines
3.2 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type {
AreaCapturePick,
CaptureArea,
CapturePick,
CapturePickerWindow,
SelectedSource,
} from "./types";
/** Smallest area worth recording, in points. */
export const MIN_CAPTURE_AREA_SIZE = 32;
type PickerDisplay = { id: number; bounds: CaptureArea };
function isFiniteNumber(value: unknown): value is number {
return typeof value === "number" && Number.isFinite(value);
}
/**
* Validates what a capture-picker window reports. An area must have a usable size
* and lie on the display it was drawn on; a window must be one the picker offered;
* a screen must be a connected display.
*/
export function normalizeCapturePick(
input: unknown,
displays: PickerDisplay[],
windows: CapturePickerWindow[],
): CapturePick | null {
if (!input || typeof input !== "object") return null;
const candidate = input as Record<string, unknown>;
const record = candidate.record === true;
const display = displays.find((entry) => entry.id === candidate.displayId);
if (!display) return null;
if (candidate.kind === "screen") {
return { kind: "screen", displayId: display.id, record };
}
if (candidate.kind === "window") {
const window = windows.find((entry) => entry.id === candidate.windowId);
return window
? { kind: "window", windowId: window.id, displayId: display.id, record }
: null;
}
if (candidate.kind !== "area") return null;
const { x, y, width, height } = candidate;
if (
!isFiniteNumber(x) ||
!isFiniteNumber(y) ||
!isFiniteNumber(width) ||
!isFiniteNumber(height)
) {
return null;
}
const left = Math.max(display.bounds.x, Math.round(x));
const top = Math.max(display.bounds.y, Math.round(y));
const right = Math.min(display.bounds.x + display.bounds.width, Math.round(x + width));
const bottom = Math.min(display.bounds.y + display.bounds.height, Math.round(y + height));
// Whole, even sizes map the area onto screen pixels one to one at any display
// scale, so the recording is never resampled.
const areaWidth = right - left - ((right - left) % 2);
const areaHeight = bottom - top - ((bottom - top) % 2);
if (areaWidth < MIN_CAPTURE_AREA_SIZE || areaHeight < MIN_CAPTURE_AREA_SIZE) {
return null;
}
return {
kind: "area",
x: left,
y: top,
width: areaWidth,
height: areaHeight,
displayId: display.id,
record,
};
}
export function createAreaSource(pick: AreaCapturePick): SelectedSource {
return {
id: `area:${pick.displayId}`,
name: `Area ${pick.width}×${pick.height}`,
display_id: String(pick.displayId),
sourceType: "area",
area: { x: pick.x, y: pick.y, width: pick.width, height: pick.height },
};
}
/** The same source the window list offers for this window. */
export function createWindowSource(window: CapturePickerWindow): SelectedSource {
const title = window.title || window.appName || "Window";
return {
id: window.id,
name: title,
display_id: window.display_id ?? "",
sourceType: "window",
appName: window.appName || undefined,
windowTitle: title,
};
}
export function getSourceArea(source: SelectedSource | null | undefined): CaptureArea | null {
if (!source?.id?.startsWith("area:") || !source.area) return null;
const { x, y, width, height } = source.area;
return [x, y, width, height].every(Number.isFinite) && width > 0 && height > 0
? { x, y, width, height }
: null;
}