Shell: a seam drag moves the whole column, not one pair
Dragging a seam moved exactly the two tiles touching it, so a column edge shared by several rows had to be dragged row by row and drifted out of line in between. A drag now moves that column boundary wherever it appears: every pair of tiles meeting at the same x, in every row, travels with it. Each pair still trades width within its own fixed span, so no row can push into another, and the whole column stops at whichever row runs out of room first — one row's minimum width holds the line for all of them. All rows move or none do; a column edge that bends halfway is worse than one that won't budge. Rows are only grouped by a shared seam. A row split somewhere else keeps its own boundary, and a row where one tile simply spans across has no seam there and stays put. Hovering a seam lights up the others that travel with it, so what is about to move is visible before the drag starts. Verified in the browser: two rows split at the same column move together from one drag; rows split at different columns stay independent; a tall tile beside two stacked ones resizes all three; and a 4-column drag stopped at 1 because the lower tile's minimum width said so. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
804dc483d6
commit
7b353cf7f8
+76
-23
@@ -531,13 +531,22 @@ function notifyResize(tile) {
|
|||||||
/* ── seam dividers between adjacent tiles ───────────────────────────── */
|
/* ── seam dividers between adjacent tiles ───────────────────────────── */
|
||||||
|
|
||||||
/* Wherever two tiles share a vertical edge, a divider sits on the seam.
|
/* Wherever two tiles share a vertical edge, a divider sits on the seam.
|
||||||
Dragging its pill moves the shared edge, trading width between the two
|
Dragging one moves that column boundary wherever it appears: every pair of
|
||||||
tiles — their combined span never changes, so nothing else can collide
|
tiles meeting at the same x, in every row, moves with it, so a column of
|
||||||
except via the explicit check below (a third tile can sit behind the
|
tiles keeps its edge as one line rather than drifting row by row. Hovering
|
||||||
seam for part of its height). */
|
a seam lights up the others that travel with it.
|
||||||
|
|
||||||
|
Each pair trades width within its own span — the pair's combined span never
|
||||||
|
changes — so nothing else can collide except via the explicit check below
|
||||||
|
(a third tile can sit behind the seam for part of its height). A row where
|
||||||
|
one tile simply spans the boundary has no seam there and stays put. */
|
||||||
|
|
||||||
|
/** Live dividers, so a drag can reposition all of them as the tiles move. */
|
||||||
|
const dividers = [];
|
||||||
|
|
||||||
function rebuildDividers() {
|
function rebuildDividers() {
|
||||||
grid.querySelectorAll('.tile-divider').forEach((el) => el.remove());
|
grid.querySelectorAll('.tile-divider').forEach((el) => el.remove());
|
||||||
|
dividers.length = 0;
|
||||||
for (const L of tiles) {
|
for (const L of tiles) {
|
||||||
for (const R of tiles) {
|
for (const R of tiles) {
|
||||||
if (L === R || L.x + L.w !== R.x) continue;
|
if (L === R || L.x + L.w !== R.x) continue;
|
||||||
@@ -549,10 +558,24 @@ function rebuildDividers() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Every pair of tiles that meets at this column boundary, in any row. */
|
||||||
|
function seamPairsAt(boundary) {
|
||||||
|
const pairs = [];
|
||||||
|
for (const L of tiles) {
|
||||||
|
if (L.x + L.w !== boundary) continue;
|
||||||
|
for (const R of tiles) {
|
||||||
|
if (R === L || R.x !== boundary) continue;
|
||||||
|
if (Math.min(L.y + L.h, R.y + R.h) <= Math.max(L.y, R.y)) continue;
|
||||||
|
pairs.push({ L, R });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pairs;
|
||||||
|
}
|
||||||
|
|
||||||
function buildDivider(L, R, top, bottom) {
|
function buildDivider(L, R, top, bottom) {
|
||||||
const el = document.createElement('div');
|
const el = document.createElement('div');
|
||||||
el.className = 'tile-divider';
|
el.className = 'tile-divider';
|
||||||
el.title = 'Drag to resize';
|
el.title = 'Drag to resize this column';
|
||||||
|
|
||||||
const place = () => {
|
const place = () => {
|
||||||
const m = cellMetrics();
|
const m = cellMetrics();
|
||||||
@@ -563,43 +586,72 @@ function buildDivider(L, R, top, bottom) {
|
|||||||
};
|
};
|
||||||
place();
|
place();
|
||||||
|
|
||||||
|
const boundary = () => L.x + L.w;
|
||||||
|
const groupEls = () => dividers.filter((d) => d.boundary() === boundary()).map((d) => d.el);
|
||||||
|
const unlink = () => { for (const d of dividers) d.el.classList.remove('linked'); };
|
||||||
|
|
||||||
|
// Before anything is dragged, show which seams travel with this one.
|
||||||
|
el.addEventListener('pointerenter', () => {
|
||||||
|
if (document.body.classList.contains('fullscreen')) return;
|
||||||
|
for (const other of groupEls()) other.classList.add('linked');
|
||||||
|
});
|
||||||
|
el.addEventListener('pointerleave', unlink);
|
||||||
|
|
||||||
el.addEventListener('pointerdown', (ev) => {
|
el.addEventListener('pointerdown', (ev) => {
|
||||||
if (ev.button !== 0 && ev.pointerType === 'mouse') return;
|
if (ev.button !== 0 && ev.pointerType === 'mouse') return;
|
||||||
if (document.body.classList.contains('fullscreen')) return;
|
if (document.body.classList.contains('fullscreen')) return;
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
closeSettingsPopovers();
|
closeSettingsPopovers();
|
||||||
el.setPointerCapture(ev.pointerId);
|
el.setPointerCapture(ev.pointerId);
|
||||||
el.classList.add('active');
|
|
||||||
const m = cellMetrics();
|
const m = cellMetrics();
|
||||||
const sx = ev.clientX;
|
const sx = ev.clientX;
|
||||||
const startBoundary = L.x + L.w;
|
const startBoundary = boundary();
|
||||||
const rightEdge = R.x + R.w; // fixed for the whole drag
|
// Every row that has a seam here moves with it. Each pair's outer edges
|
||||||
const minB = L.x + minSizeOf(L).w;
|
// are fixed for the whole drag, so each row only trades width internally.
|
||||||
const maxB = rightEdge - minSizeOf(R).w;
|
const parts = seamPairsAt(startBoundary).map(({ L: a, R: b }) => ({
|
||||||
|
L: a, R: b, leftX: a.x, rightEdge: b.x + b.w,
|
||||||
|
}));
|
||||||
|
if (!parts.length) return;
|
||||||
|
const moving = parts.flatMap((p) => [p.L.id, p.R.id]);
|
||||||
|
// The whole column stops at whichever row runs out of room first.
|
||||||
|
const minB = Math.max(...parts.map((p) => p.leftX + minSizeOf(p.L).w));
|
||||||
|
const maxB = Math.min(...parts.map((p) => p.rightEdge - minSizeOf(p.R).w));
|
||||||
|
const group = groupEls();
|
||||||
|
for (const other of group) other.classList.add('active');
|
||||||
let changed = false;
|
let changed = false;
|
||||||
|
|
||||||
const onMove = (e) => {
|
const onMove = (e) => {
|
||||||
const b = Math.max(minB, Math.min(maxB,
|
const b = Math.max(minB, Math.min(maxB,
|
||||||
startBoundary + Math.round((e.clientX - sx) / (m.cw + m.gap))));
|
startBoundary + Math.round((e.clientX - sx) / (m.cw + m.gap))));
|
||||||
if (b === L.x + L.w) return;
|
if (b === boundary()) return;
|
||||||
const candL = { x: L.x, y: L.y, w: b - L.x, h: L.h };
|
const next = parts.map((p) => ({
|
||||||
const candR = { x: b, y: R.y, w: rightEdge - b, h: R.h };
|
p,
|
||||||
if (!isFreeExcept(candL, [L.id, R.id]) || !isFreeExcept(candR, [L.id, R.id])) return;
|
candL: { x: p.leftX, y: p.L.y, w: b - p.leftX, h: p.L.h },
|
||||||
L.w = candL.w;
|
candR: { x: b, y: p.R.y, w: p.rightEdge - b, h: p.R.h },
|
||||||
R.x = candR.x;
|
}));
|
||||||
R.w = candR.w;
|
// All rows move or none do — a column edge that bends is worse than one
|
||||||
applyRect(L);
|
// that won't budge.
|
||||||
applyRect(R);
|
if (next.some(({ candL, candR }) =>
|
||||||
notifyResize(L);
|
!isFreeExcept(candL, moving) || !isFreeExcept(candR, moving))) return;
|
||||||
notifyResize(R);
|
for (const { p, candL, candR } of next) {
|
||||||
place();
|
p.L.w = candL.w;
|
||||||
|
p.R.x = candR.x;
|
||||||
|
p.R.w = candR.w;
|
||||||
|
applyRect(p.L);
|
||||||
|
applyRect(p.R);
|
||||||
|
notifyResize(p.L);
|
||||||
|
notifyResize(p.R);
|
||||||
|
}
|
||||||
|
for (const d of dividers) d.place();
|
||||||
changed = true;
|
changed = true;
|
||||||
};
|
};
|
||||||
const onUp = () => {
|
const onUp = () => {
|
||||||
el.removeEventListener('pointermove', onMove);
|
el.removeEventListener('pointermove', onMove);
|
||||||
el.removeEventListener('pointerup', onUp);
|
el.removeEventListener('pointerup', onUp);
|
||||||
el.removeEventListener('pointercancel', onUp);
|
el.removeEventListener('pointercancel', onUp);
|
||||||
el.classList.remove('active');
|
for (const other of group) other.classList.remove('active');
|
||||||
|
unlink();
|
||||||
if (changed) saveLayout();
|
if (changed) saveLayout();
|
||||||
rebuildDividers(); // adjacencies may have changed
|
rebuildDividers(); // adjacencies may have changed
|
||||||
};
|
};
|
||||||
@@ -608,6 +660,7 @@ function buildDivider(L, R, top, bottom) {
|
|||||||
el.addEventListener('pointercancel', onUp);
|
el.addEventListener('pointercancel', onUp);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
dividers.push({ el, place, boundary });
|
||||||
grid.appendChild(el);
|
grid.appendChild(el);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -494,7 +494,8 @@ body.fullscreen .tile.head-peek .tile-notch { top: 32px; }
|
|||||||
transition: opacity 0.15s ease, background 0.15s ease;
|
transition: opacity 0.15s ease, background 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tile-divider:hover::after { opacity: 0.7; }
|
.tile-divider:hover::after,
|
||||||
|
.tile-divider.linked::after { opacity: 0.7; }
|
||||||
.tile-divider.active::after { opacity: 1; background: var(--text); }
|
.tile-divider.active::after { opacity: 1; background: var(--text); }
|
||||||
|
|
||||||
/* Hide the pills while a tile itself is being moved or corner-resized —
|
/* Hide the pills while a tile itself is being moved or corner-resized —
|
||||||
|
|||||||
Reference in New Issue
Block a user