mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-17 00:34:01 -04:00
control center: let grid drops land where dropped
This commit is contained in:
@@ -131,44 +131,33 @@ function cellAt(layout, x, y, cols, rows) {
|
||||
};
|
||||
}
|
||||
|
||||
// With gravity, a tile dragged down onto another and dropped at its row gets lifted straight back above it. So a
|
||||
// tile coming from above lands just past the tiles it overlaps, and one fully covering a single tile that fits
|
||||
// where it came from trades places with it.
|
||||
function dropInto(items, cells, index, target) {
|
||||
// With gravity, a tile dropped onto the stack it came from rises back toward the hole it left. So the drop tries
|
||||
// the target row and the rows below it, packing each, and keeps the one that settles nearest the target.
|
||||
function dropInto(items, cells, index, target, pack, step = 1) {
|
||||
const origin = cells[index];
|
||||
if (!origin || !target)
|
||||
if (!origin || !target || !pack)
|
||||
return items;
|
||||
const box = cell => ({
|
||||
"x": cell.col,
|
||||
"y": cell.row,
|
||||
"w": cell.cols,
|
||||
"h": cell.rows
|
||||
});
|
||||
const moved = {
|
||||
"x": target.col,
|
||||
"y": target.row,
|
||||
"w": origin.cols,
|
||||
"h": origin.rows
|
||||
};
|
||||
if (overlaps([box(origin)], moved.x, moved.y, moved.w, moved.h))
|
||||
if (target.col < origin.col + origin.cols && origin.col < target.col + origin.cols && target.row < origin.row + origin.rows && origin.row < target.row + origin.rows)
|
||||
return items;
|
||||
const hits = cells.filter((cell, i) => i !== index && cell && overlaps([box(cell)], moved.x, moved.y, moved.w, moved.h));
|
||||
if (hits.length === 0)
|
||||
return items;
|
||||
if (hits.every(cell => cell.row >= origin.row + origin.rows)) {
|
||||
const row = hits.reduce((bottom, cell) => Math.max(bottom, cell.row + cell.rows), moved.y);
|
||||
return items.map((item, i) => i === index ? Object.assign({}, item, {
|
||||
const limit = cells.reduce((bottom, cell) => cell ? Math.max(bottom, cell.row + cell.rows) : bottom, 0) + origin.rows;
|
||||
let best = null;
|
||||
for (let row = target.row; row <= limit; row += step) {
|
||||
const trial = items.map((item, i) => i === index ? Object.assign({}, item, {
|
||||
"row": row
|
||||
}) : item);
|
||||
const landed = pack(trial)[index]?.row;
|
||||
if (landed === undefined)
|
||||
return items;
|
||||
const distance = Math.abs(landed - target.row);
|
||||
if (!best || distance < best.distance || (distance === best.distance && landed >= target.row))
|
||||
best = {
|
||||
"trial": trial,
|
||||
"distance": distance
|
||||
};
|
||||
if (landed >= target.row)
|
||||
break;
|
||||
}
|
||||
const other = hits.length === 1 ? hits[0] : null;
|
||||
const covered = other && other.col >= moved.x && other.row >= moved.y && other.col + other.cols <= moved.x + moved.w && other.row + other.rows <= moved.y + moved.h;
|
||||
if (!covered || other.cols > origin.cols || other.rows > origin.rows)
|
||||
return items;
|
||||
return items.map((item, i) => cells[i] === other ? Object.assign({}, item, {
|
||||
"col": origin.col,
|
||||
"row": origin.row
|
||||
}) : item);
|
||||
return best.trial;
|
||||
}
|
||||
|
||||
function placedItems(items, slots) {
|
||||
|
||||
@@ -38,12 +38,18 @@ DEditableGrid {
|
||||
readonly property var shownIndices: savedWidgets.reduce((indices, widget, i) => !WidgetUtils.inFooter(widget) && WidgetUtils.isShown(widget) && model?.componentForWidget(widget) ? indices.concat([i]) : indices, [])
|
||||
|
||||
sourceItems: shownIndices.map(i => Object.assign({}, savedWidgets[i], sizeWithHiddenTwin(i)))
|
||||
slotLayout: GridUtils.packCards(layoutItems.map(widget => Object.assign({}, widget, WidgetUtils.clampSize(widget, columns, maximumRows))), placementOrder, columns, width, CcMetrics.gridGap, cellWidth - CcMetrics.gridGap, I18n.isRtl, null, CcMetrics.gridStep, true)
|
||||
swapDrags: true
|
||||
slotLayout: GridUtils.packCards(clamped(layoutItems), placementOrder, columns, width, CcMetrics.gridGap, cellWidth - CcMetrics.gridGap, I18n.isRtl, null, CcMetrics.gridStep, true)
|
||||
gravity: true
|
||||
rowStep: CcMetrics.gridStep
|
||||
packer: items => GridUtils.packCells(clamped(items), placementOrder, columns, null, CcMetrics.gridStep, true).cells
|
||||
placeholderRadius: draggingSlot?.small ? Theme.fullRadius(draggingSlot.width, draggingSlot.height) : (draggingSlot?.tileItem?.bodyRadius ?? Theme.fullRadius(width, CcMetrics.tileHeight))
|
||||
|
||||
onLayoutCommitted: items => model.setLayout(withHidden(items))
|
||||
|
||||
function clamped(items) {
|
||||
return items.map(widget => Object.assign({}, widget, WidgetUtils.clampSize(widget, columns, maximumRows)));
|
||||
}
|
||||
|
||||
function sizeWithHiddenTwin(index) {
|
||||
const widget = savedWidgets[index];
|
||||
const size = WidgetUtils.clampSize(widget, Infinity);
|
||||
|
||||
@@ -184,12 +184,18 @@ FocusScope {
|
||||
}
|
||||
|
||||
// Goes by the dragged tile, not the pointer: the middle of its top row has to cross the grid's edge as it
|
||||
// was at drag start, which the grid growing under the drag cannot move.
|
||||
// was at drag start, which the grid growing under the drag cannot move. Below the grid that edge sits one
|
||||
// row lower while the panel has room to grow, so a tile can still be dropped into a new bottom row.
|
||||
function footerTakesGridDrag(tile) {
|
||||
if (gridDragWidget === null || footer.freeCells() < WidgetUtils.footerMinCells(gridDragWidget.id))
|
||||
return false;
|
||||
const anchor = tile.y + Math.min(tile.height, widgetGrid.slotLayout.rowUnit) / 2;
|
||||
return footerOnTop ? anchor < -CcMetrics.gridGap / 2 : anchor > widgetGrid.pinnedHeight + CcMetrics.gridGap / 2;
|
||||
const rowUnit = widgetGrid.slotLayout.rowUnit;
|
||||
const anchor = tile.y + Math.min(tile.height, rowUnit) / 2;
|
||||
if (footerOnTop)
|
||||
return anchor < -CcMetrics.gridGap / 2;
|
||||
const newRow = CcMetrics.gridGap + rowUnit;
|
||||
const room = widgetGrid.pinnedHeight + newRow <= availableGridHeight ? newRow : 0;
|
||||
return anchor > widgetGrid.pinnedHeight + room + CcMetrics.gridGap / 2;
|
||||
}
|
||||
|
||||
// Drops commit a tick later: committing inside the release handler would destroy the dragged item mid-signal.
|
||||
|
||||
@@ -32,7 +32,12 @@ Item {
|
||||
property var dropHandler: null
|
||||
// An item hovering from outside, placed first so the grid makes room for it.
|
||||
property var externalItem: null
|
||||
property bool swapDrags: false
|
||||
// The layout compacts upward, so a drop has to account for the hole the dragged item leaves.
|
||||
property bool gravity: false
|
||||
// Takes the items with the dragged one at its target and returns their packed cells, so a drop can tell
|
||||
// whether gravity would lift the dragged item back above where it was dropped.
|
||||
property var packer: null
|
||||
property real rowStep: 1
|
||||
readonly property alias tileModel: tiles
|
||||
readonly property bool interacting: draggingSourceIndex >= 0 || sizePreview !== null || externalItem !== null
|
||||
readonly property real layoutHeight: interacting ? Math.max(heldHeight, slotLayout.totalHeight) : slotLayout.totalHeight
|
||||
@@ -54,7 +59,7 @@ Item {
|
||||
if (interactingIndex < 0 || !changes)
|
||||
return items;
|
||||
const changed = items.map((item, i) => i === interactingIndex ? Object.assign({}, item, changes) : item);
|
||||
return swapDrags && dragCell && draggingSourceIndex >= 0 ? GridUtils.dropInto(changed, pinnedCells, draggingSourceIndex, dragCell) : changed;
|
||||
return gravity && dragCell && draggingSourceIndex >= 0 ? GridUtils.dropInto(changed, pinnedCells, draggingSourceIndex, dragCell, packer, rowStep) : changed;
|
||||
}
|
||||
|
||||
signal layoutCommitted(var items)
|
||||
|
||||
@@ -81,18 +81,37 @@ test("panel step follows the pointer and holds on a pinned edge", () => {
|
||||
assert.equal(grid.nearestStep(rightEdge, 20, 6, 30, 1400), 20);
|
||||
});
|
||||
|
||||
test("a tile dragged down lands past the tile it covers instead of back above it", () => {
|
||||
const items = [{ id: "a" }, { id: "b" }, { id: "c" }];
|
||||
const dropped = (cells, index, target, columns = 2, step = 1) => {
|
||||
const items = cells.map((cell, i) => ({ id: String(i), w: cell.cols, h: cell.rows, col: cell.col, row: cell.row }));
|
||||
const moved = items.map((item, i) => i === index ? Object.assign({}, item, target) : item);
|
||||
const order = [index].concat(items.map((item, i) => i).filter(i => i !== index));
|
||||
const pack = trial => grid.packCells(trial, order, columns, null, step, true).cells;
|
||||
return plain(pack(grid.dropInto(moved, cells, index, target, pack, step)).map(cell => [cell.col, cell.row]));
|
||||
};
|
||||
|
||||
test("a tile dragged down its own stack lands where dropped instead of back above the tile it covers", () => {
|
||||
const cells = [{ col: 0, row: 0, cols: 1, rows: 1 }, { col: 0, row: 1, cols: 1, rows: 1 }, { col: 0, row: 2, cols: 1, rows: 2 }];
|
||||
assert.deepEqual(plain(grid.dropInto(items, cells, 0, { col: 0, row: 1 })), [{ id: "a", row: 2 }, { id: "b" }, { id: "c" }]);
|
||||
assert.deepEqual(plain(grid.dropInto(items, cells, 0, { col: 0, row: 2 })), [{ id: "a", row: 4 }, { id: "b" }, { id: "c" }]);
|
||||
assert.deepEqual(plain(grid.dropInto(items, cells, 2, { col: 0, row: 0 })), items);
|
||||
assert.deepEqual(plain(grid.dropInto(items, cells, 1, { col: 0, row: 2 })), [{ id: "a" }, { id: "b", row: 4 }, { id: "c" }]);
|
||||
assert.deepEqual(dropped(cells, 0, { col: 0, row: 1 }), [[0, 1], [0, 0], [0, 2]]);
|
||||
assert.deepEqual(dropped(cells, 0, { col: 0, row: 2 }), [[0, 3], [0, 0], [0, 1]]);
|
||||
assert.deepEqual(dropped(cells, 2, { col: 0, row: 0 }), [[0, 2], [0, 3], [0, 0]]);
|
||||
assert.deepEqual(dropped(cells, 1, { col: 0, row: 2 }), [[0, 0], [0, 3], [0, 1]]);
|
||||
});
|
||||
|
||||
test("a tile dropped sideways or up onto a same-size tile trades places with it", () => {
|
||||
const items = [{ id: "a" }, { id: "b" }, { id: "c" }];
|
||||
const cells = [{ col: 0, row: 0, cols: 1, rows: 1 }, { col: 1, row: 0, cols: 1, rows: 1 }, { col: 1, row: 1, cols: 1, rows: 1 }];
|
||||
assert.deepEqual(plain(grid.dropInto(items, cells, 0, { col: 1, row: 0 })), [{ id: "a" }, { id: "b", col: 0, row: 0 }, { id: "c" }]);
|
||||
assert.deepEqual(plain(grid.dropInto(items, cells, 2, { col: 1, row: 0 })), [{ id: "a" }, { id: "b", col: 1, row: 1 }, { id: "c" }]);
|
||||
test("a half-width tile dragged down beside a blocked stack moves one step per half row", () => {
|
||||
const cells = [{ col: 0, row: 0, cols: 1, rows: 1 }, { col: 1, row: 0, cols: 0.5, rows: 1 }, { col: 1.5, row: 0, cols: 0.5, rows: 1 }, { col: 0, row: 1, cols: 2, rows: 1 }, { col: 0, row: 2, cols: 2, rows: 1 }];
|
||||
const rowOf = target => dropped(cells, 1, { col: 1, row: target }, 2, 0.5)[1][1];
|
||||
assert.deepEqual([1, 1.5, 2, 2.5, 3].map(rowOf), [2, 2, 2, 3, 3]);
|
||||
});
|
||||
|
||||
test("a tile dragged down from another column lands where dropped unless gravity would lift it back", () => {
|
||||
const cells = [{ col: 0, row: 0, cols: 1, rows: 1 }, { col: 0, row: 1, cols: 1, rows: 1 }, { col: 0, row: 2, cols: 1, rows: 1 }, { col: 1, row: 0, cols: 1, rows: 1 }];
|
||||
const items = cells.map((cell, i) => ({ id: "abcd"[i], col: cell.col, row: cell.row }));
|
||||
const drop = (index, target) => {
|
||||
const moved = items.map((item, i) => i === index ? Object.assign({}, item, target) : item);
|
||||
const order = [index].concat(items.map((item, i) => i).filter(i => i !== index));
|
||||
const pack = trial => grid.packCells(trial, order, 2, null, 1, true).cells;
|
||||
return plain(grid.packCells(grid.dropInto(moved, cells, index, target, pack), order, 2, null, 1, true).cells.map(cell => [cell.col, cell.row]));
|
||||
};
|
||||
assert.deepEqual(drop(3, { col: 0, row: 1 }), [[0, 0], [0, 2], [0, 3], [0, 1]]);
|
||||
assert.deepEqual(drop(0, { col: 0, row: 1 }), [[0, 1], [0, 0], [0, 2], [1, 0]]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user