fix desktop icon grid gap at right edge: stretch column pitch so right-anchored stacks sit flush

This commit is contained in:
Martin Dvořák 2026-09-24 07:34:48 +00:00
commit eecf8cf339
2 changed files with 56 additions and 20 deletions

View file

@ -53,11 +53,20 @@ export function useDesktopIcons(
const [positions, setPositions] = useState<Record<string, IconPosition>>({});
const [dragId, setDragId] = useState<string | null>(null);
const cellRef = useRef({ w: DEFAULT_CELL_WIDTH, h: DEFAULT_CELL_HEIGHT });
const gridRef = useRef<{ cols: number; rows: number; originX: number; originY: number }>({
const gridRef = useRef<{
cols: number;
rows: number;
originX: number;
originY: number;
pitchX: number;
pitchY: number;
}>({
cols: 1,
rows: 1,
originX: 10,
originY: 12,
pitchX: DEFAULT_CELL_WIDTH + GRID_GAP,
pitchY: DEFAULT_CELL_HEIGHT + GRID_GAP,
});
const suppressRef = useRef(false);
@ -71,8 +80,10 @@ export function useDesktopIcons(
/**
* Work out the grid from where the launcher stacks actually sit. The icon
* width/height (plus the flex gap) is the cell pitch, and the left/top stack
* offsets give column 0 / row 0. Falls back to sensible values before mount.
* width/height is the cell size and the left/top stack offsets give column 0 /
* row 0; the column pitch is stretched a touch so the right-hand stacks stay
* flush with the right margin instead of stopping up to a cell short. Falls
* back to sensible values before mount.
*/
const measure = useCallback(() => {
const root = rootRef.current;
@ -86,11 +97,13 @@ export function useDesktopIcons(
// Each stack renders from its own top offset; the left stack defines row 0,
// the top-right stack shares it and the bottom-right stack defines the last
// row band. Use the smallest top as the grid origin.
// row band. Use the smallest top as the grid origin. The right-anchored
// stacks also give the true right margin.
let cellW = 0;
let cellH = 0;
let originX = Infinity;
let originY = Infinity;
let rightMargin = Infinity;
stacks.forEach((stack) => {
const stackRect = stack.getBoundingClientRect();
@ -109,45 +122,60 @@ export function useDesktopIcons(
const r = el.getBoundingClientRect();
cellW = Math.max(cellW, r.width);
cellH = Math.max(cellH, r.height);
rightMargin = Math.min(rightMargin, rootRect.right - r.right);
});
});
if (!Number.isFinite(originX)) originX = 10;
if (!Number.isFinite(originY)) originY = 12;
if (!Number.isFinite(rightMargin)) rightMargin = 10;
if (cellW <= 0 || cellH <= 0) return;
const pitchX = cellW + GRID_GAP;
const pitchY = cellH + GRID_GAP;
const width = root.clientWidth || window.innerWidth;
const height = (root.clientHeight || window.innerHeight) - TASKBAR_HEIGHT;
// Columns tile from the left, but the services/friends stacks are anchored
// to the right edge. A fixed pitch leaves up to a full cell of slack on the
// right (the remainder of width / pitch), which pulled those icons away from
// the border. Stretch the column pitch a hair so the first and last columns
// land flush on both margins; the extra gap is a few px and stays uniform.
const minPitchX = cellW + GRID_GAP;
const span = width - originX - rightMargin;
const cols = Math.max(1, Math.floor((span + GRID_GAP) / minPitchX));
let pitchX = minPitchX;
if (cols > 1) {
const stretched = (span - cellW) / (cols - 1);
if (stretched > pitchX) pitchX = stretched;
}
cellRef.current = { w: cellW, h: cellH };
gridRef.current = {
cols: Math.max(1, Math.floor((width - originX) / pitchX)),
cols,
rows: Math.max(1, Math.floor((height - originY) / pitchY)),
originX,
originY,
pitchX,
pitchY,
};
}, [rootRef]);
const posToCell = useCallback(
(p: IconPosition): Cell => {
const { w: cellW, h: cellH } = cellRef.current;
const g = gridRef.current;
return clampCell(
Math.round((p.x - g.originX) / (cellW + GRID_GAP)),
Math.round((p.y - g.originY) / (cellH + GRID_GAP))
Math.round((p.x - g.originX) / g.pitchX),
Math.round((p.y - g.originY) / g.pitchY)
);
},
[clampCell]
);
const cellToPos = useCallback((c: Cell): IconPosition => {
const { w: cellW, h: cellH } = cellRef.current;
const g = gridRef.current;
return {
x: g.originX + c.col * (cellW + GRID_GAP),
y: g.originY + c.row * (cellH + GRID_GAP),
x: g.originX + c.col * g.pitchX,
y: g.originY + c.row * g.pitchY,
};
}, []);
@ -217,7 +245,6 @@ export function useDesktopIcons(
const defaultCells = useCallback(
(keys: string[]): Record<string, IconPosition> => {
const root = rootRef.current;
const { w: cellW, h: cellH } = cellRef.current;
const g = gridRef.current;
const out: Record<string, IconPosition> = {};
if (!root) return out;
@ -225,8 +252,8 @@ export function useDesktopIcons(
const stacks = Array.from(
root.querySelectorAll<HTMLElement>(".win-icons")
).filter((el) => el.querySelector("[data-drag-id]"));
const pitchX = cellW + GRID_GAP;
const pitchY = cellH + GRID_GAP;
const pitchX = g.pitchX;
const pitchY = g.pitchY;
const rootRect = root.getBoundingClientRect();
// Natural cell for each icon: the one its stack actually renders it in.
@ -267,8 +294,8 @@ export function useDesktopIcons(
const { w: cellW, h: cellH } = cellRef.current;
const g = gridRef.current;
return clampCell(
Math.floor((x - g.originX + GRID_GAP / 2) / (cellW + GRID_GAP)),
Math.floor((y - g.originY + GRID_GAP / 2) / (cellH + GRID_GAP))
Math.floor((x - g.originX + (g.pitchX - cellW) / 2) / g.pitchX),
Math.floor((y - g.originY + (g.pitchY - cellH) / 2) / g.pitchY)
);
},
[clampCell]