// Fields beyond mode/scale/transform/x/y/primary are optional on a record. // Absent means "this layout says nothing about it", which every check below // treats as valid: arrangements stored before the color and mirror fields // existed must keep validating. const colorProfiles = ["auto", "srgb", "wide", "hdr"]; const vrrModes = [-1, 0, 1, 2]; const bitdepths = [8, 10]; const sdrBrightnessRange = { min: 0.8, max: 2.0 }; const sdrSaturationRange = { min: 0.8, max: 1.2 }; function validColorProfile(value) { return colorProfiles.indexOf(value) >= 0; } function validVrrMode(value) { return Number.isInteger(value) && vrrModes.indexOf(value) >= 0; } function validBitdepth(value) { return Number.isInteger(value) && bitdepths.indexOf(value) >= 0; } function inRange(value, range) { return Number.isFinite(value) && value >= range.min && value <= range.max; } function validSdrBrightness(value) { return inRange(value, sdrBrightnessRange); } function validSdrSaturation(value) { return inRange(value, sdrSaturationRange); } // "" for a display that is not mirroring, so callers never branch on undefined. function mirrorTarget(record) { return record && typeof record.mirrorOf === "string" ? record.mirrorOf : ""; } function isMirrored(record) { return mirrorTarget(record) !== ""; } function logicalSize(record) { if (!record) return { width: 0, height: 0 }; const width = Number(record.width); const height = Number(record.height); const scale = Number(record.scale); const transform = Number(record.transform); if (!Number.isFinite(width) || !Number.isFinite(height) || !Number.isFinite(scale) || scale <= 0) return { width: 0, height: 0 }; const rotated = transform === 1 || transform === 3; return { width: (rotated ? height : width) / scale, height: (rotated ? width : height) / scale }; } function validCoordinate(value) { return Number.isFinite(value) && Number.isInteger(value) && value >= -100000 && value <= 100000; } function validOptionalFields(record) { if (record.vrrMode !== undefined && !validVrrMode(record.vrrMode)) return false; if (record.colorProfile !== undefined && !validColorProfile(record.colorProfile)) return false; if (record.bitdepth !== undefined && !validBitdepth(record.bitdepth)) return false; if (record.sdrBrightness !== undefined && !validSdrBrightness(record.sdrBrightness)) return false; if (record.sdrSaturation !== undefined && !validSdrSaturation(record.sdrSaturation)) return false; if (record.mirrorOf !== undefined && typeof record.mirrorOf !== "string") return false; return !isMirrored(record) || /^[A-Za-z0-9_.-]+$/.test(record.mirrorOf); } function validate(layout) { if (!Array.isArray(layout) || layout.length === 0) return false; const names = {}; let primaryCount = 0; for (const record of layout) { if (!record || typeof record.name !== "string" || !/^[A-Za-z0-9_.-]+$/.test(record.name) || names[record.name]) return false; names[record.name] = true; if (!Number.isFinite(record.width) || record.width <= 0 || !Number.isFinite(record.height) || record.height <= 0 || !Number.isFinite(record.scale) || record.scale <= 0 || !Number.isInteger(record.transform) || record.transform < 0 || record.transform > 3 || !validCoordinate(record.x) || !validCoordinate(record.y) || typeof record.primary !== "boolean" || !validOptionalFields(record)) return false; const size = logicalSize(record); if (!Number.isFinite(size.width) || size.width <= 0 || !Number.isFinite(size.height) || size.height <= 0) return false; if (record.primary) primaryCount += 1; } // Mirroring is checked once every name is known. A mirror needs a target // that is part of this same layout and is not itself a mirror, because // Hyprland has no chain to follow; and the primary may not mirror, since // the arrangement is anchored on it and a mirror has no position of its own. for (const record of layout) { const target = mirrorTarget(record); if (target === "") continue; if (record.primary === true || target === record.name) return false; const other = layout.find(candidate => candidate.name === target); if (!other || isMirrored(other)) return false; } return primaryCount === 1; } function cloneLayout(layout) { return (layout || []).map(record => Object.assign({}, record)); } // A mirrored output is placed by the compositor, not by us, so it is left out // of every geometry pass: shifting it, measuring the desktop by it, or snapping // to it would all be arithmetic on a position nothing reads back. function placedRecords(layout) { const placed = (layout || []).filter(record => !isMirrored(record)); return placed.length > 0 ? placed : (layout || []); } function normalize(layout) { const result = cloneLayout(layout); const primary = result.find(record => record.primary === true); if (!primary) return result; const anchorX = primary.x; const anchorY = primary.y; for (const record of result) { if (isMirrored(record)) continue; record.x -= anchorX; record.y -= anchorY; } // A mirror sits on its target, so it is given the target's place rather // than a shifted version of coordinates that stopped meaning anything the // moment mirroring was turned on. Nothing asserts them -- the compositor // chooses -- but a stale pair would still be drawn on the canvas. for (const record of result) { if (!isMirrored(record)) continue; const target = result.find(candidate => candidate.name === record.mirrorOf); if (!target) continue; record.x = target.x; record.y = target.y; } return result; } function bounds(layout) { if (!Array.isArray(layout) || layout.length === 0) return { x: 0, y: 0, width: 0, height: 0 }; let left = Infinity; let top = Infinity; let right = -Infinity; let bottom = -Infinity; for (const record of placedRecords(layout)) { const size = logicalSize(record); left = Math.min(left, record.x); top = Math.min(top, record.y); right = Math.max(right, record.x + size.width); bottom = Math.max(bottom, record.y + size.height); } return { x: left, y: top, width: right - left, height: bottom - top }; } function snap(layout, movingName, threshold) { const result = cloneLayout(layout); const moving = result.find(record => record.name === movingName); if (!moving || isMirrored(moving)) return result; const limit = Number.isFinite(threshold) && threshold >= 0 ? threshold : 16; const movingSize = logicalSize(moving); const movingXEdges = [moving.x, moving.x + movingSize.width]; const movingYEdges = [moving.y, moving.y + movingSize.height]; const stationary = result .filter(record => record.name !== movingName && !isMirrored(record)) .sort((left, right) => left.name.localeCompare(right.name)); let bestX = null; let bestY = null; for (const record of stationary) { const size = logicalSize(record); const xEdges = [record.x, record.x + size.width]; const yEdges = [record.y, record.y + size.height]; for (const source of movingXEdges) { for (const target of xEdges) { const delta = target - source; const distance = Math.abs(delta); if (distance <= limit && (bestX === null || distance < bestX.distance)) bestX = { delta, distance }; } } for (const source of movingYEdges) { for (const target of yEdges) { const delta = target - source; const distance = Math.abs(delta); if (distance <= limit && (bestY === null || distance < bestY.distance)) bestY = { delta, distance }; } } } if (bestX !== null) moving.x += bestX.delta; if (bestY !== null) moving.y += bestY.delta; return result; } function canvasRects(layout, canvasWidth, canvasHeight, padding) { const desktopBounds = bounds(layout); const inset = Math.max(0, Number(padding) || 0); const availableWidth = Math.max(0, Number(canvasWidth) - inset * 2); const availableHeight = Math.max(0, Number(canvasHeight) - inset * 2); const scale = desktopBounds.width > 0 && desktopBounds.height > 0 ? Math.min(availableWidth / desktopBounds.width, availableHeight / desktopBounds.height) : 0; const contentWidth = desktopBounds.width * scale; const contentHeight = desktopBounds.height * scale; const originX = inset + (availableWidth - contentWidth) / 2; const originY = inset + (availableHeight - contentHeight) / 2; const rects = (layout || []).map(record => { const size = logicalSize(record); return { name: record.name, x: originX + (record.x - desktopBounds.x) * scale, y: originY + (record.y - desktopBounds.y) * scale, width: size.width * scale, height: size.height * scale, primary: record.primary === true, mirrorOf: mirrorTarget(record), mirrored: isMirrored(record) }; }); // A mirror shows the same picture in the same place as its target, so it is // drawn stacked on it with a badge rather than at coordinates of its own. // Its target is guaranteed present by validate(); an unvalidated layout that // names a missing one keeps its own rectangle instead of vanishing. for (const rect of rects) { if (!rect.mirrored) continue; const target = rects.find(candidate => candidate.name === rect.mirrorOf); if (!target) continue; rect.x = target.x; rect.y = target.y; rect.width = target.width; rect.height = target.height; } return { bounds: desktopBounds, scale, rects }; }