Files
DankMaterialShell/quickshell/Common/GridLayout.js
T

220 lines
6.8 KiB
JavaScript

function packCells(cards, order, columns, isAvailable, step = 1) {
const steps = Math.round(columns / step);
const cells = [];
const taken = [];
let rows = 0;
for (let p = 0; p < order.length; p++) {
const sourceIndex = order[p];
const card = cards[sourceIndex];
if (!card)
continue;
if (isAvailable && !isAvailable(card.id)) {
cells[sourceIndex] = null;
continue;
}
const w = Math.max(1, Math.min(steps, Math.round((card.w || 1) / step)));
const h = Math.max(1, Math.round((card.h || 1) / step));
const cell = positioned(card) ? settle(taken, Math.max(0, Math.min(steps - w, Math.round(card.col / step))), Math.max(0, Math.round(card.row / step)), w, h) : firstFit(taken, steps, w, h);
taken.push(cell);
rows = Math.max(rows, cell.y + cell.h);
cells[sourceIndex] = {
"col": cell.x * step,
"row": cell.y * step,
"cols": w * step,
"rows": h * step
};
}
return {
"cells": cells,
"rows": rows * step
};
}
function positioned(card) {
return Number.isFinite(card.col) && Number.isFinite(card.row);
}
function overlaps(taken, x, y, w, h) {
return taken.some(cell => x < cell.x + cell.w && cell.x < x + w && y < cell.y + cell.h && cell.y < y + h);
}
function settle(taken, x, y, w, h) {
while (overlaps(taken, x, y, w, h))
y++;
return {
"x": x,
"y": y,
"w": w,
"h": h
};
}
function firstFit(taken, steps, w, h) {
const ceiling = taken.reduce((top, cell) => Math.max(top, cell.y + cell.h), 0);
for (let y = 0; y < ceiling; y++) {
for (let x = 0; x + w <= steps; x++) {
if (!overlaps(taken, x, y, w, h))
return {
"x": x,
"y": y,
"w": w,
"h": h
};
}
}
return {
"x": 0,
"y": ceiling,
"w": w,
"h": h
};
}
function packCards(cards, order, columns, width, gap, rowUnit, mirror, isAvailable, step = 1) {
const packed = packCells(cards, order, columns, isAvailable, step);
const colW = (width - gap * (columns - 1)) / columns;
const slots = packed.cells.map(cell => {
if (!cell)
return null;
const px = cell.col * (colW + gap);
const pw = cell.cols * colW + (cell.cols - 1) * gap;
return {
"x": mirror ? width - px - pw : px,
"y": cell.row * (rowUnit + gap),
"w": pw,
"h": cell.rows * rowUnit + (cell.rows - 1) * gap,
"col": cell.col,
"row": cell.row,
"cols": cell.cols,
"rows": cell.rows
};
});
return {
"slots": slots,
"rows": packed.rows,
"totalHeight": packed.rows > 0 ? packed.rows * rowUnit + (packed.rows - 1) * gap : 0,
"columns": columns,
"width": width,
"colW": colW,
"rowUnit": rowUnit,
"gap": gap,
"step": step,
"mirror": mirror
};
}
function cellAt(layout, x, y, cols, rows) {
const px = layout.mirror ? layout.width - x - (cols * layout.colW + (cols - 1) * layout.gap) : x;
const col = Math.round(px / (layout.colW + layout.gap) / layout.step) * layout.step;
const row = Math.round(y / (layout.rowUnit + layout.gap) / layout.step) * layout.step;
return {
"col": Math.max(0, Math.min(layout.columns - cols, col)),
"row": Math.max(0, row)
};
}
function placedItems(items, slots) {
return items.map((item, i) => slots[i] ? Object.assign({}, item, {
"col": slots[i].col,
"row": slots[i].row
}) : item);
}
function rowLimit(cards, order, columns, rows, isAvailable) {
return Math.max(rows, packCells(cards, order, columns, isAvailable).rows);
}
function fitWithin(cards, order, columns, limit, index, want, min, isAvailable) {
let best = null;
for (let w = Math.min(columns, want.w); w >= min.w; w--) {
for (let h = want.h; h >= min.h; h--) {
const trial = cards.slice();
trial[index] = Object.assign({}, cards[index], {
"w": w,
"h": h
});
if (packCells(trial, order, columns, isAvailable).rows > limit)
continue;
const distance = want.w - w + want.h - h;
const better = !best || distance < best.distance || (distance === best.distance && w * h > best.w * best.h);
if (better)
best = {
"w": w,
"h": h,
"distance": distance
};
break;
}
}
return best ? {
"w": best.w,
"h": best.h
} : null;
}
function fitResize(cards, order, columns, rows, index, want, min, isAvailable) {
const limit = rowLimit(cards, order, columns, rows, isAvailable);
return fitWithin(cards, order, columns, limit, index, want, min, isAvailable);
}
function fitNewCard(cards, columns, rows, id, want, min, isAvailable) {
const order = cards.map((card, i) => i);
const limit = rowLimit(cards, order, columns, rows, isAvailable);
const trial = cards.concat([
{
"id": id,
"w": want.w,
"h": want.h
}
]);
return fitWithin(trial, order.concat([cards.length]), columns, limit, cards.length, want, min, isAvailable);
}
function neighborInDirection(rects, index, direction) {
const from = rects[index];
let best = -1;
let bestScore = Infinity;
rects.forEach((rect, i) => {
if (i === index)
return;
const score = directionalScore(from, rect, direction);
if (score >= bestScore)
return;
bestScore = score;
best = i;
});
return best;
}
function directionalScore(from, to, direction) {
switch (direction) {
case "left":
return axisScore(from.x - (to.x + to.w), crossGap(from.y, from.h, to.y, to.h));
case "right":
return axisScore(to.x - (from.x + from.w), crossGap(from.y, from.h, to.y, to.h));
case "up":
return axisScore(from.y - (to.y + to.h), crossGap(from.x, from.w, to.x, to.w));
case "down":
return axisScore(to.y - (from.y + from.h), crossGap(from.x, from.w, to.x, to.w));
}
return Infinity;
}
function axisScore(gap, cross) {
return gap < 0 ? Infinity : gap + cross * 2;
}
function crossGap(a, aSize, b, bSize) {
return Math.max(0, b - (a + aSize), a - (b + bSize));
}
function dimension(value, minimum, maximum, fallback, step = 1) {
const min = minimum ?? 1;
const max = Math.max(min, maximum ?? fallback ?? min);
return Math.max(min, Math.min(max, typeof value === "number" && Number.isInteger(value / step) ? value : (fallback ?? min)));
}