Rebuild Displays around the canvas, and let the transaction keep color
Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
@@ -1,3 +1,46 @@
|
||||
// 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 };
|
||||
@@ -22,6 +65,22 @@ function validCoordinate(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;
|
||||
@@ -41,7 +100,8 @@ function validate(layout) {
|
||||
|| !Number.isInteger(record.transform)
|
||||
|| record.transform < 0 || record.transform > 3
|
||||
|| !validCoordinate(record.x) || !validCoordinate(record.y)
|
||||
|| typeof record.primary !== "boolean")
|
||||
|| typeof record.primary !== "boolean"
|
||||
|| !validOptionalFields(record))
|
||||
return false;
|
||||
|
||||
const size = logicalSize(record);
|
||||
@@ -51,6 +111,21 @@ function validate(layout) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -58,6 +133,14 @@ 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);
|
||||
@@ -66,9 +149,24 @@ function normalize(layout) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -80,7 +178,7 @@ function bounds(layout) {
|
||||
let top = Infinity;
|
||||
let right = -Infinity;
|
||||
let bottom = -Infinity;
|
||||
for (const record of layout) {
|
||||
for (const record of placedRecords(layout)) {
|
||||
const size = logicalSize(record);
|
||||
left = Math.min(left, record.x);
|
||||
top = Math.min(top, record.y);
|
||||
@@ -93,7 +191,7 @@ function bounds(layout) {
|
||||
function snap(layout, movingName, threshold) {
|
||||
const result = cloneLayout(layout);
|
||||
const moving = result.find(record => record.name === movingName);
|
||||
if (!moving)
|
||||
if (!moving || isMirrored(moving))
|
||||
return result;
|
||||
|
||||
const limit = Number.isFinite(threshold) && threshold >= 0 ? threshold : 16;
|
||||
@@ -101,7 +199,7 @@ function snap(layout, movingName, threshold) {
|
||||
const movingXEdges = [moving.x, moving.x + movingSize.width];
|
||||
const movingYEdges = [moving.y, moving.y + movingSize.height];
|
||||
const stationary = result
|
||||
.filter(record => record.name !== movingName)
|
||||
.filter(record => record.name !== movingName && !isMirrored(record))
|
||||
.sort((left, right) => left.name.localeCompare(right.name));
|
||||
|
||||
let bestX = null;
|
||||
@@ -148,19 +246,35 @@ function canvasRects(layout, canvasWidth, canvasHeight, padding) {
|
||||
const contentHeight = desktopBounds.height * scale;
|
||||
const originX = inset + (availableWidth - contentWidth) / 2;
|
||||
const originY = inset + (availableHeight - contentHeight) / 2;
|
||||
return {
|
||||
bounds: desktopBounds,
|
||||
scale,
|
||||
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
|
||||
};
|
||||
})
|
||||
};
|
||||
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 };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user