mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-11-09 07:23:14 +00:00
"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>
108 lines
3.2 KiB
TypeScript
108 lines
3.2 KiB
TypeScript
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;
|
||
}
|