fix desktop icon grid gap at right edge: stretch column pitch so right-anchored stacks sit flush
This commit is contained in:
parent
74a442d9fd
commit
eecf8cf339
2 changed files with 56 additions and 20 deletions
15
opencode.md
15
opencode.md
|
|
@ -121,9 +121,13 @@ with icons, it's almost certainly here.
|
||||||
`.win-icons-tr` / `.win-icons-br`). They are rendered **hidden** with class
|
`.win-icons-tr` / `.win-icons-br`). They are rendered **hidden** with class
|
||||||
`win-measure` purely so the grid can be measured from them.
|
`win-measure` purely so the grid can be measured from them.
|
||||||
- On mount (a `useLayoutEffect`), `measure()` reads the real rendered boxes of
|
- On mount (a `useLayoutEffect`), `measure()` reads the real rendered boxes of
|
||||||
those stacks: cell size = icon width/height **+ the CSS flex gap** (`GRID_GAP`,
|
those stacks: cell size = icon width/height, and origin = the left stack's
|
||||||
currently 4px), and origin = the left stack's offset. From that it computes
|
offset. The row pitch is cell height **+ the CSS flex gap** (`GRID_GAP`,
|
||||||
`cols` / `rows` for the whole desktop.
|
currently 4px). The column pitch starts at cell width + gap but is **stretched
|
||||||
|
a hair** so the grid spans from the left margin to the right margin — a fixed
|
||||||
|
pitch left up to a full cell of slack on the right, which pulled the
|
||||||
|
right-anchored services/friends stacks away from the border. From that it
|
||||||
|
computes `cols` / `rows` for the whole desktop.
|
||||||
- `defaultCells()` assigns each icon the cell its stack already occupies. So
|
- `defaultCells()` assigns each icon the cell its stack already occupies. So
|
||||||
**the grid you see on load is the grid dragging uses** — no re-layout jump.
|
**the grid you see on load is the grid dragging uses** — no re-layout jump.
|
||||||
- Icons are then rendered absolutely positioned in `.win-icon-layer`, driven by
|
- Icons are then rendered absolutely positioned in `.win-icon-layer`, driven by
|
||||||
|
|
@ -183,6 +187,11 @@ list reordering, not free placement. Don't confuse the two.
|
||||||
6. **Friend icons couldn't be dragged.** Native image drag on the
|
6. **Friend icons couldn't be dragged.** Native image drag on the
|
||||||
`next/image` glyph fired `pointercancel`. Fixed with `draggable={false}` +
|
`next/image` glyph fired `pointercancel`. Fixed with `draggable={false}` +
|
||||||
`-webkit-user-drag: none`.
|
`-webkit-user-drag: none`.
|
||||||
|
7. **Right-edge gap.** Columns tiled from the left with a fixed pitch, so the
|
||||||
|
right-anchored stacks (services/friends) stopped up to a full cell short of
|
||||||
|
the right border (e.g. ~90px on a 1920px screen), and any remount/resize
|
||||||
|
re-snapped them. Fixed by stretching the column pitch so the grid spans both
|
||||||
|
margins; the row pitch is unchanged.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -53,11 +53,20 @@ export function useDesktopIcons(
|
||||||
const [positions, setPositions] = useState<Record<string, IconPosition>>({});
|
const [positions, setPositions] = useState<Record<string, IconPosition>>({});
|
||||||
const [dragId, setDragId] = useState<string | null>(null);
|
const [dragId, setDragId] = useState<string | null>(null);
|
||||||
const cellRef = useRef({ w: DEFAULT_CELL_WIDTH, h: DEFAULT_CELL_HEIGHT });
|
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,
|
cols: 1,
|
||||||
rows: 1,
|
rows: 1,
|
||||||
originX: 10,
|
originX: 10,
|
||||||
originY: 12,
|
originY: 12,
|
||||||
|
pitchX: DEFAULT_CELL_WIDTH + GRID_GAP,
|
||||||
|
pitchY: DEFAULT_CELL_HEIGHT + GRID_GAP,
|
||||||
});
|
});
|
||||||
const suppressRef = useRef(false);
|
const suppressRef = useRef(false);
|
||||||
|
|
||||||
|
|
@ -71,8 +80,10 @@ export function useDesktopIcons(
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Work out the grid from where the launcher stacks actually sit. The icon
|
* 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
|
* width/height is the cell size and the left/top stack offsets give column 0 /
|
||||||
* offsets give column 0 / row 0. Falls back to sensible values before mount.
|
* 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 measure = useCallback(() => {
|
||||||
const root = rootRef.current;
|
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,
|
// 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
|
// 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 cellW = 0;
|
||||||
let cellH = 0;
|
let cellH = 0;
|
||||||
let originX = Infinity;
|
let originX = Infinity;
|
||||||
let originY = Infinity;
|
let originY = Infinity;
|
||||||
|
let rightMargin = Infinity;
|
||||||
|
|
||||||
stacks.forEach((stack) => {
|
stacks.forEach((stack) => {
|
||||||
const stackRect = stack.getBoundingClientRect();
|
const stackRect = stack.getBoundingClientRect();
|
||||||
|
|
@ -109,45 +122,60 @@ export function useDesktopIcons(
|
||||||
const r = el.getBoundingClientRect();
|
const r = el.getBoundingClientRect();
|
||||||
cellW = Math.max(cellW, r.width);
|
cellW = Math.max(cellW, r.width);
|
||||||
cellH = Math.max(cellH, r.height);
|
cellH = Math.max(cellH, r.height);
|
||||||
|
rightMargin = Math.min(rightMargin, rootRect.right - r.right);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!Number.isFinite(originX)) originX = 10;
|
if (!Number.isFinite(originX)) originX = 10;
|
||||||
if (!Number.isFinite(originY)) originY = 12;
|
if (!Number.isFinite(originY)) originY = 12;
|
||||||
|
if (!Number.isFinite(rightMargin)) rightMargin = 10;
|
||||||
if (cellW <= 0 || cellH <= 0) return;
|
if (cellW <= 0 || cellH <= 0) return;
|
||||||
|
|
||||||
const pitchX = cellW + GRID_GAP;
|
|
||||||
const pitchY = cellH + GRID_GAP;
|
const pitchY = cellH + GRID_GAP;
|
||||||
const width = root.clientWidth || window.innerWidth;
|
const width = root.clientWidth || window.innerWidth;
|
||||||
const height = (root.clientHeight || window.innerHeight) - TASKBAR_HEIGHT;
|
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 };
|
cellRef.current = { w: cellW, h: cellH };
|
||||||
gridRef.current = {
|
gridRef.current = {
|
||||||
cols: Math.max(1, Math.floor((width - originX) / pitchX)),
|
cols,
|
||||||
rows: Math.max(1, Math.floor((height - originY) / pitchY)),
|
rows: Math.max(1, Math.floor((height - originY) / pitchY)),
|
||||||
originX,
|
originX,
|
||||||
originY,
|
originY,
|
||||||
|
pitchX,
|
||||||
|
pitchY,
|
||||||
};
|
};
|
||||||
}, [rootRef]);
|
}, [rootRef]);
|
||||||
|
|
||||||
const posToCell = useCallback(
|
const posToCell = useCallback(
|
||||||
(p: IconPosition): Cell => {
|
(p: IconPosition): Cell => {
|
||||||
const { w: cellW, h: cellH } = cellRef.current;
|
|
||||||
const g = gridRef.current;
|
const g = gridRef.current;
|
||||||
return clampCell(
|
return clampCell(
|
||||||
Math.round((p.x - g.originX) / (cellW + GRID_GAP)),
|
Math.round((p.x - g.originX) / g.pitchX),
|
||||||
Math.round((p.y - g.originY) / (cellH + GRID_GAP))
|
Math.round((p.y - g.originY) / g.pitchY)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[clampCell]
|
[clampCell]
|
||||||
);
|
);
|
||||||
|
|
||||||
const cellToPos = useCallback((c: Cell): IconPosition => {
|
const cellToPos = useCallback((c: Cell): IconPosition => {
|
||||||
const { w: cellW, h: cellH } = cellRef.current;
|
|
||||||
const g = gridRef.current;
|
const g = gridRef.current;
|
||||||
return {
|
return {
|
||||||
x: g.originX + c.col * (cellW + GRID_GAP),
|
x: g.originX + c.col * g.pitchX,
|
||||||
y: g.originY + c.row * (cellH + GRID_GAP),
|
y: g.originY + c.row * g.pitchY,
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
@ -217,7 +245,6 @@ export function useDesktopIcons(
|
||||||
const defaultCells = useCallback(
|
const defaultCells = useCallback(
|
||||||
(keys: string[]): Record<string, IconPosition> => {
|
(keys: string[]): Record<string, IconPosition> => {
|
||||||
const root = rootRef.current;
|
const root = rootRef.current;
|
||||||
const { w: cellW, h: cellH } = cellRef.current;
|
|
||||||
const g = gridRef.current;
|
const g = gridRef.current;
|
||||||
const out: Record<string, IconPosition> = {};
|
const out: Record<string, IconPosition> = {};
|
||||||
if (!root) return out;
|
if (!root) return out;
|
||||||
|
|
@ -225,8 +252,8 @@ export function useDesktopIcons(
|
||||||
const stacks = Array.from(
|
const stacks = Array.from(
|
||||||
root.querySelectorAll<HTMLElement>(".win-icons")
|
root.querySelectorAll<HTMLElement>(".win-icons")
|
||||||
).filter((el) => el.querySelector("[data-drag-id]"));
|
).filter((el) => el.querySelector("[data-drag-id]"));
|
||||||
const pitchX = cellW + GRID_GAP;
|
const pitchX = g.pitchX;
|
||||||
const pitchY = cellH + GRID_GAP;
|
const pitchY = g.pitchY;
|
||||||
const rootRect = root.getBoundingClientRect();
|
const rootRect = root.getBoundingClientRect();
|
||||||
|
|
||||||
// Natural cell for each icon: the one its stack actually renders it in.
|
// 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 { w: cellW, h: cellH } = cellRef.current;
|
||||||
const g = gridRef.current;
|
const g = gridRef.current;
|
||||||
return clampCell(
|
return clampCell(
|
||||||
Math.floor((x - g.originX + GRID_GAP / 2) / (cellW + GRID_GAP)),
|
Math.floor((x - g.originX + (g.pitchX - cellW) / 2) / g.pitchX),
|
||||||
Math.floor((y - g.originY + GRID_GAP / 2) / (cellH + GRID_GAP))
|
Math.floor((y - g.originY + (g.pitchY - cellH) / 2) / g.pitchY)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[clampCell]
|
[clampCell]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue