From 11f99cc13f4d8c07979e4b5bf22d42e26712e6cd Mon Sep 17 00:00:00 2001 From: bbedward Date: Thu, 22 Oct 2026 23:59:52 -0400 Subject: [PATCH] displays: ceil logical output sizes on niri so neighbours never overlap fixes #3875 port 2.0 (cherry picked from commit acc2c947e5ad4cc830eb85a6c8d2c1935182f5b1) --- quickshell/Common/OutputModel.js | 31 +++++++++++++------------- quickshell/tests/output-model.test.mjs | 13 ++++++++--- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/quickshell/Common/OutputModel.js b/quickshell/Common/OutputModel.js index aa8e433e1..0f02907e7 100644 --- a/quickshell/Common/OutputModel.js +++ b/quickshell/Common/OutputModel.js @@ -833,6 +833,16 @@ function physicalSize(output) { }; } +function roundLogical(value, compositor) { + switch (compositor) { + case "niri": + // niri rounds the placed output, smithay's Space ceils mapped ones, only ceil never overlaps in either order (#3875) + return Math.ceil(value); + default: + return Math.round(value); + } +} + function logicalSize(output, compositor) { if (!output) return { @@ -841,19 +851,10 @@ function logicalSize(output, compositor) { }; const phys = physicalSize(output); const scale = output.logical?.scale || 1.0; - switch (compositor) { - case "niri": - // niri floors logical sizes, rounding up by one leaves a dead seam at snapped edges (#2526) - return { - "w": Math.floor(phys.w / scale), - "h": Math.floor(phys.h / scale) - }; - default: - return { - "w": Math.round(phys.w / scale), - "h": Math.round(phys.h / scale) - }; - } + return { + "w": roundLogical(phys.w / scale, compositor), + "h": roundLogical(phys.h / scale, compositor) + }; } function defaultBounds() { @@ -991,8 +992,8 @@ function recalculateAdjacentPositions(outputs, pendingChanges, changedOutput, ne if (!output?.logical) return []; const oldPhys = physicalSize(output); - const oldLogicalW = Math.round(oldPhys.w / (output.logical.scale || 1.0)); - const newLogicalW = Math.round(oldPhys.w / newScale); + const oldLogicalW = roundLogical(oldPhys.w / (output.logical.scale || 1.0), compositor); + const newLogicalW = roundLogical(oldPhys.w / newScale, compositor); const changedX = pendingChanges[changedOutput]?.position?.x ?? output.logical.x; const changedY = pendingChanges[changedOutput]?.position?.y ?? output.logical.y; const moves = []; diff --git a/quickshell/tests/output-model.test.mjs b/quickshell/tests/output-model.test.mjs index d6595eabf..473a67128 100644 --- a/quickshell/tests/output-model.test.mjs +++ b/quickshell/tests/output-model.test.mjs @@ -201,15 +201,16 @@ test("aqueous heads: fingerprint, head list and comparison tolerate scale noise assert.equal(model.outputHeadsMatch(customMode, wlr.outputs, wlr.outputs), true); }); -test("sizes: rotation swaps, niri floors and hyprland rounds logical sizes, bounds default to 1920x1080", () => { +test("sizes: rotation swaps, niri ceils and hyprland rounds logical sizes, bounds default to 1920x1080", () => { assert.deepEqual(model.physicalSize(live["DP-1"]), { w: 3840, h: 2160 }); assert.deepEqual(model.physicalSize(live["eDP-1"]), { w: 1200, h: 1920 }); assert.deepEqual(model.physicalSize(null), { w: 1920, h: 1080 }); assert.deepEqual(model.logicalSize(live["DP-1"], "niri"), { w: 2560, h: 1440 }); - assert.deepEqual(model.logicalSize(live["eDP-1"], "niri"), { w: 923, h: 1476 }); + assert.deepEqual(model.logicalSize(live["eDP-1"], "niri"), { w: 924, h: 1477 }); + assert.deepEqual(model.logicalSize({ modes: [{ width: 2560, height: 1600 }], current_mode: 0, logical: { scale: 1.75 } }, "niri"), { w: 1463, h: 915 }); assert.deepEqual(model.logicalSize(live["eDP-1"], "hyprland"), { w: 923, h: 1477 }); assert.deepEqual(model.logicalSize({ logical: { width: 2560, height: 1440, scale: 2, transform: "90" } }, "niri"), { w: 1440, h: 2560 }); - assert.deepEqual(model.outputBounds(live, "niri"), { minX: 0, minY: 0, maxX: 3483, maxY: 1476, width: 3483, height: 1476 }); + assert.deepEqual(model.outputBounds(live, "niri"), { minX: 0, minY: 0, maxX: 3484, maxY: 1477, width: 3484, height: 1477 }); assert.deepEqual(model.outputBounds(live, "hyprland").maxY, 1477); assert.deepEqual(model.outputBounds({}, "niri"), { minX: 0, minY: 0, maxX: 1920, maxY: 1080, width: 1920, height: 1080 }); }); @@ -222,6 +223,12 @@ test("canvas: overlap ignores disabled outputs, snapping to neighbour edges, adj assert.equal(model.checkOverlap(layout, "DP-1", 0, 0, 10, 10), false); assert.equal(model.checkOverlap({ outputs: live, disabled: {}, compositor: "hyprland" }, "DP-1", 0, 0, 10, 10), true); assert.deepEqual([[2570, 30], [3000, 3000], [2400, 100], [1000, 1500], [-900, 1300]].map(([x, y]) => model.snapToEdges(layout, "eDP-1", x, y, 923, 1476)), [{ x: 2560, y: 0 }, { x: 3000, y: 3000 }, { x: 2560, y: 0 }, { x: 1000, y: 1440 }, { x: -923, y: 1440 }]); + const issue3875 = { compositor: "niri", disabled: {}, outputs: { + "DP-1": { modes: [{ width: 3440, height: 1440 }], current_mode: 0, logical: { x: 0, y: 0, scale: 1 } }, + "eDP-1": { modes: [{ width: 2560, height: 1600 }], current_mode: 0, logical: { x: 3440, y: 526, scale: 1.75 } }, + "DP-2": { modes: [{ width: 5120, height: 1440 }], current_mode: 0, logical: { x: 10022, y: 0, scale: 1 } } + } }; + assert.deepEqual(model.snapToEdges(issue3875, "DP-2", 4890, -5, 5120, 1440), { x: 4903, y: 0 }); assert.deepEqual(model.recalculateAdjacentPositions(live, {}, "DP-1", 2, "niri"), [{ name: "eDP-1", x: 1920, y: 0 }]); assert.deepEqual(model.recalculateAdjacentPositions(live, { "eDP-1": { position: { x: 2563, y: 5 } } }, "eDP-1", 1, "niri"), [{ name: "eDP-1", x: 2560, y: 5 }]); assert.deepEqual(model.recalculateAdjacentPositions(live, {}, "HDMI-A-1", 1, "niri"), []);