refactor(control center): free the edit button and misc tweaks

This commit is contained in:
purian23 committed 2026-10-04 00:55:31 -04:00
1 parent 585827e7e4
commit b48fce0447
15 files changed
+212 -124

No files matched your search

+15 -6
View File
@@ -131,9 +131,10 @@ function cellAt(layout, x, y, cols, rows) {
};
}
// With gravity, a tile dropped onto the one below it gets lifted straight back into the hole it left, so a tile
// fully covering exactly one other that fits where it came from trades places with it instead.
function swapInto(items, cells, index, target) {
// 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) {
const origin = cells[index];
if (!origin || !target)
return items;
@@ -151,12 +152,20 @@ function swapInto(items, cells, index, target) {
};
if (overlaps([box(origin)], moved.x, moved.y, moved.w, moved.h))
return items;
const hits = cells.reduce((found, cell, i) => i !== index && cell && overlaps([box(cell)], moved.x, moved.y, moved.w, moved.h) ? found.concat([i]) : found, []);
const other = hits.length === 1 ? cells[hits[0]] : null;
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, {
"row": row
}) : item);
}
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) => i === hits[0] ? Object.assign({}, item, {
return items.map((item, i) => cells[i] === other ? Object.assign({}, item, {
"col": origin.col,
"row": origin.row
}) : item);
+1 -1
View File
@@ -21,7 +21,7 @@ Singleton {
id: root
readonly property var log: Log.scoped("SettingsData")
readonly property int settingsConfigVersion: 36
readonly property int settingsConfigVersion: 37
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
@@ -544,6 +544,15 @@ var LOCAL_SPEC = {
w: 4,
h: 1,
footer: true
},
{
id: "edit",
enabled: true,
w: 1,
h: 1,
small: true,
footer: true,
footerEnd: true
}
]
},
@@ -707,6 +707,12 @@ function migrateToVersion(obj, targetVersion) {
settings.configVersion = 35;
}
if (currentVersion < 37 && targetVersion >= 37) {
if (Array.isArray(settings.controlCenterWidgets) && !settings.controlCenterWidgets.some(widget => widget?.id === "edit"))
settings.controlCenterWidgets = settings.controlCenterWidgets.concat([Object.assign({}, SpecModule.SPEC.controlCenterWidgets.def.find(widget => widget.id === "edit"))]);
settings.configVersion = 37;
}
return settings;
}
@@ -12,6 +12,7 @@ DankGridEditChrome {
signal configRequested(var anchor)
hasOptions: WidgetUtils.hasOptions(widgetData.id)
removable: WidgetUtils.isRemovable(widgetData)
buttonSize: Theme.iconSize
hitOverflow: CcMetrics.gridGap / 2
iconSize: PopoutMetrics.chromeIconSize
@@ -34,38 +34,43 @@ Item {
property var gridSlot: null
property var resizePreview: null
property Item flyingItem: null
property bool incomingRemovable: true
readonly property bool dragging: gridDragging || liftedIndex >= 0
readonly property bool dragRemovable: liftedIndex >= 0 ? WidgetUtils.isRemovable(items.find(item => item.index === liftedIndex)?.widget) : incomingRemovable
readonly property bool overTrash: trashContains(liftedIndex >= 0 ? liftedPoint : gridDragPoint)
signal addWidgetRequested
signal resetRequested
signal clearRequested
signal moveRequested
signal editToggled
signal finishRequested
signal cancelRequested
signal removeRequested(int index)
signal configRequested(int index, var widgetData, var anchor)
signal itemMoved(int index, rect sceneRect, bool leaving)
signal resized(int index, int cells, bool fill)
signal resized(int index, real cells, bool fill)
// Grid columns, so footer widths line up with the tiles above.
readonly property real spacing: CcMetrics.gridGap
readonly property real pitch: grid.cellWidth
readonly property real editActionsWidth: Theme.iconButtonSize * 2 + spacing
readonly property real trackWidth: Math.max(0, width - trailing.width - CcMetrics.footerGap)
// Edit mode shows the whole row scaled into the room beside the edit buttons, so every width stays reachable.
readonly property real editScale: trackWidth > 0 ? Math.min(1, (trackWidth - editActionsWidth - CcMetrics.footerGap) / trackWidth) : 1
readonly property real editActionsWidth: Theme.iconButtonSize * 3 + spacing * 2
// The row owns its full width in both modes, so capacity never changes under the user's feet; edit mode
// shows the whole row scaled into the room beside the edit buttons, so every width stays reachable.
readonly property real trackWidth: width
readonly property real editScale: width > 0 ? Math.min(1, (width - editActionsWidth - CcMetrics.footerGap) / width) : 1
readonly property real trackScale: editMode ? editScale : 1
readonly property int capacity: Math.max(0, Math.floor((trackWidth + spacing) / pitch))
readonly property real step: CcMetrics.gridStep
readonly property real capacity: Math.max(0, Math.floor((trackWidth + spacing) / pitch / step + 1e-6) * step)
readonly property var fills: items.map(item => resizePreview?.index === item.index ? resizePreview.fill : WidgetUtils.footerFills(item.widget))
readonly property var mins: items.map(item => WidgetUtils.footerMinCells(item.widget.id))
readonly property var ends: items.map(item => WidgetUtils.footerEnds(item.widget))
readonly property var keeps: items.map(item => !WidgetUtils.isRemovable(item.widget))
// Fill items count at their minimum, so a drop or a neighbour growing takes room from them first.
readonly property var fixedCells: WidgetUtils.fitFooterCells(items.map((item, i) => fills[i] ? mins[i] : (resizePreview?.index === item.index ? resizePreview.cells : WidgetUtils.footerCells(item.widget))), mins, capacity)
readonly property int fixedUsed: fixedCells.reduce((sum, count) => sum + count, 0)
readonly property var cells: WidgetUtils.spreadFooterFill(fixedCells, fills, capacity - fixedUsed - (incoming?.cells ?? 0))
readonly property var fixedCells: WidgetUtils.fitFooterCells(items.map((item, i) => fills[i] ? mins[i] : (resizePreview?.index === item.index ? resizePreview.cells : WidgetUtils.footerCells(item.widget))), mins, capacity, keeps, step)
readonly property real fixedUsed: fixedCells.reduce((sum, count) => sum + count, 0)
readonly property var cells: WidgetUtils.spreadFooterFill(fixedCells, fills, capacity - fixedUsed - (incoming?.cells ?? 0), step)
readonly property int slackIndex: fills.findIndex((fill, i) => fill && cells[i] > 0)
// The first fill item also takes the part of a cell left over, so it ends exactly at the next button.
// The first fill item also takes the part of a cell left over, so it ends exactly at the next item or the edge.
readonly property real fillSlack: slackIndex < 0 ? 0 : Math.max(0, trackWidth + spacing - (cells.reduce((sum, count) => sum + count, 0) + (incoming?.cells ?? 0)) * pitch)
// Stable keys keep delegates alive across resizes and reorders, so they update in place instead of rebuilding.
readonly property var keys: {
@@ -109,7 +114,7 @@ Item {
}
function trashContains(scenePosition) {
if (!editMode || !dragging)
if (!editMode || !dragging || !dragRemovable)
return false;
return trash.contains(trash.mapFromItem(null, scenePosition.x, scenePosition.y));
}
@@ -275,7 +280,7 @@ Item {
"index": -1,
"widget": ({})
})
readonly property int cells: root.cells[index] ?? 0
readonly property real cells: root.cells[index] ?? 0
readonly property bool lifted: root.liftedIndex === entry.index
readonly property real span: root.spanWidth(Math.max(1, cells)) + (index === root.slackIndex ? root.fillSlack : 0)
readonly property real restX: root.xFor(root.layout.offsets[index] ?? 0, span)
@@ -337,7 +342,7 @@ Item {
id: chrome
property real startX: 0
property int startCells: 0
property real startCells: 0
property bool startFill: false
anchors.fill: parent
@@ -361,12 +366,12 @@ Item {
"fill": startFill
};
}
// Pulling past the edit buttons turns the item into a fill, which runs on to the pencil outside edit mode.
// Pulling past the edit buttons turns the item into a fill, which runs on to the row's edge outside edit mode.
onResizeMoved: (px, py) => {
const delta = (mapToItem(null, px, py).x - startX) * (I18n.isRtl ? -1 : 1) / root.trackScale;
const min = root.mins[footerItem.index];
const max = Math.max(min, root.capacity - (root.fixedUsed - root.fixedCells[footerItem.index]));
const wanted = Math.round(startCells + delta / root.pitch);
const wanted = Math.round((startCells + delta / root.pitch) / root.step) * root.step;
root.resizePreview = {
"index": footerItem.entry.index,
"cells": Math.max(min, Math.min(max, wanted)),
@@ -420,65 +425,85 @@ Item {
}
}
Row {
anchors.right: trailing.left
anchors.rightMargin: CcMetrics.footerGap
anchors.verticalCenter: parent.verticalCenter
spacing: root.spacing
visible: root.editMode && !root.dragging
Item {
id: editActions
DankActionButton {
buttonSize: Theme.iconButtonSize
iconName: "add"
iconSize: CcMetrics.iconBoxIconSize
iconColor: Theme.onSecondaryContainer
backgroundColor: Theme.secondaryContainer
tooltipText: I18n.tr("Add widget")
onClicked: root.addWidgetRequested()
}
DankActionButton {
id: moreButton
buttonSize: Theme.iconButtonSize
iconName: "more_horiz"
iconSize: CcMetrics.iconBoxIconSize
iconColor: CcMetrics.tileInactiveContent
backgroundColor: CcMetrics.tileInactiveColor
border.width: Theme.layerOutlineWidth
border.color: Theme.outlineMedium
tooltipText: I18n.tr("More")
onClicked: editMenu.openAt(moreButton)
}
}
StyledRect {
id: trash
anchors.right: trailing.left
anchors.rightMargin: CcMetrics.footerGap
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
width: root.editActionsWidth
height: CcMetrics.footerHeight
radius: Theme.fullRadius(width, height)
color: root.overTrash ? Theme.errorContainer : CcMetrics.tileInactiveColor
visible: root.editMode && root.dragging
Accessible.role: Accessible.Graphic
Accessible.name: I18n.tr("Remove")
visible: root.editMode
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
spacing: root.spacing
visible: !root.dragging
DankActionButton {
buttonSize: Theme.iconButtonSize
iconName: "add"
iconSize: CcMetrics.iconBoxIconSize
iconColor: Theme.onSecondaryContainer
backgroundColor: Theme.secondaryContainer
tooltipText: I18n.tr("Add widget")
onClicked: root.addWidgetRequested()
}
DankActionButton {
id: moreButton
buttonSize: Theme.iconButtonSize
iconName: "more_horiz"
iconSize: CcMetrics.iconBoxIconSize
iconColor: CcMetrics.tileInactiveContent
backgroundColor: CcMetrics.tileInactiveColor
border.width: Theme.layerOutlineWidth
border.color: Theme.outlineMedium
tooltipText: I18n.tr("More")
onClicked: editMenu.openAt(moreButton)
}
}
DankIcon {
anchors.centerIn: parent
name: "delete"
size: CcMetrics.iconBoxIconSize
filled: root.overTrash
color: root.overTrash ? Theme.onErrorContainer : CcMetrics.tileInactiveContent
StyledRect {
id: trash
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
width: root.editActionsWidth - Theme.iconButtonSize - root.spacing
height: CcMetrics.footerHeight
radius: Theme.fullRadius(width, height)
color: root.overTrash ? Theme.errorContainer : CcMetrics.tileInactiveColor
visible: root.dragging && root.dragRemovable
Accessible.role: Accessible.Graphic
Accessible.name: I18n.tr("Remove")
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
DankIcon {
anchors.centerIn: parent
name: "delete"
size: CcMetrics.iconBoxIconSize
filled: root.overTrash
color: root.overTrash ? Theme.onErrorContainer : CcMetrics.tileInactiveContent
}
}
DankActionButton {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
buttonSize: Theme.iconButtonSize
iconName: "check"
iconSize: CcMetrics.iconBoxIconSize
iconColor: Theme.onPrimary
backgroundColor: Theme.primary
tooltipText: I18n.tr("Save")
onClicked: root.finishRequested()
}
}
@@ -489,7 +514,7 @@ Item {
items: [
{
"iconName": root.onTop ? "vertical_align_bottom" : "vertical_align_top",
"label": root.onTop ? I18n.tr("Move down") : I18n.tr("Move up"),
"label": root.onTop ? I18n.tr("Move row to footer") : I18n.tr("Move row to header"),
"action": () => root.moveRequested()
},
{
@@ -511,42 +536,6 @@ Item {
]
}
Item {
id: trailing
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
// One column, the same pill as a small button docked beside it.
width: root.spanWidth(1)
height: CcMetrics.footerHeight
DankActionButton {
anchors.fill: parent
buttonSize: CcMetrics.footerHeight
iconName: "check"
iconSize: CcMetrics.iconBoxIconSize
iconColor: Theme.onPrimary
backgroundColor: Theme.primary
tooltipText: I18n.tr("Save")
visible: root.editMode
onClicked: root.editToggled()
}
DankActionButton {
anchors.fill: parent
buttonSize: CcMetrics.footerHeight
iconName: "edit"
iconSize: CcMetrics.iconBoxIconSize
iconColor: CcMetrics.tileInactiveContent
backgroundColor: CcMetrics.tileInactiveColor
border.width: Theme.layerOutlineWidth
border.color: Theme.outlineMedium
tooltipText: I18n.tr("Edit")
visible: !root.editMode
onClicked: root.editToggled()
}
}
// The track's layer only draws inside the row, so a lifted item is shown from here while it travels.
ShaderEffectSource {
readonly property real pad: PopoutMetrics.editOverflow
@@ -24,6 +24,7 @@ DankEditableGrid {
signal lockRequested
signal powerRequested
signal settingsRequested
signal editRequested
signal accountsRequested
signal closeRequested
property bool tapToClose: false
@@ -88,7 +88,7 @@ DankEditableGridSlot {
dragging: root.dragging
resizing: root.resizing
cornerRadius: root.small ? Theme.fullRadius(root.width, root.height) : (root.tileItem?.bodyRadius ?? Theme.fullRadius(root.width, root.height))
sizeText: root.cols + "×" + root.rows
sizeText: root.cols + "×" + (root.small ? CcMetrics.smallRowFraction : root.rows)
onResizeStarted: (px, py) => {
root.biasH = root.small ? (CcMetrics.smallRowFraction - 1) * root.grid.cellWidth : 0;
root.beginResize(px, py);
@@ -74,6 +74,9 @@ FocusScope {
implicitHeight: targetImplicitHeight
focus: true
Component.onCompleted: WidgetUtils.ensureEditButton()
onPlacedWidgetIdsChanged: WidgetUtils.ensureEditButton()
function navigateTo(section) {
if (section === host.expandedSection)
return;
@@ -279,6 +282,13 @@ FocusScope {
event.accepted = true;
}
Shortcut {
sequences: ["F2", "Ctrl+E"]
enabled: root.host.shouldBeVisible && !root.host.editMode && !root.pageOpen
context: Qt.WindowShortcut
onActivated: root.host.editMode = true
}
readonly property string expandedSection: host.expandedSection ?? ""
readonly property bool editMode: host.editMode
@@ -416,6 +426,7 @@ FocusScope {
onColorPickerRequested: root.host.openColorPicker()
onCloseRequested: root.host.close()
onSettingsRequested: root.host.openSettings()
onEditRequested: root.host.editMode = true
onAccountsRequested: root.host.openAccounts()
onLockRequested: {
root.host.close();
@@ -449,6 +460,7 @@ FocusScope {
items: root.footerItems
gridDragging: root.gridDragWidget !== null
gridDragPoint: widgetGrid.dragScenePoint
incomingRemovable: WidgetUtils.isRemovable(root.gridDragWidget)
incoming: root.gridDragWidget !== null && widgetGrid.heldOutside && !footer.overTrash ? Object.assign({
"cells": Math.min(WidgetUtils.footerCells(root.gridDragWidget), footer.freeCells())
}, footer.gridSlot) : null
@@ -463,7 +475,7 @@ FocusScope {
onConfigRequested: (index, widgetData, anchor) => root.openConfigOverlay(index, widgetData, anchor)
onItemMoved: (index, sceneRect, leaving) => root.previewFooterDrag(index, sceneRect, leaving)
onResized: (index, cells, fill) => WidgetUtils.setFooterSize(index, cells, fill)
onEditToggled: root.host.editMode = !root.host.editMode
onFinishRequested: root.host.editMode = false
onCancelRequested: root.cancelEdit()
}
@@ -171,6 +171,7 @@ QtObject {
case "lock":
case "power":
case "settings":
case "edit":
return actionTile;
case "runningApps":
return runningAppsTile;
@@ -228,6 +229,15 @@ QtObject {
"category": "system",
"enabled": true
},
{
"id": "edit",
"text": I18n.tr("Edit"),
"description": I18n.tr("Edit widgets"),
"icon": "edit",
"type": "action",
"category": "system",
"enabled": true
},
{
"id": "runningApps",
"text": I18n.tr("Running apps"),
@@ -23,6 +23,9 @@ CcTile {
case "settings":
host?.settingsRequested();
return;
case "edit":
host?.editRequested();
return;
}
}
}
@@ -2,9 +2,12 @@
.import qs.Services as Services
.import qs.Modules.ControlCenter as ControlCenter
.import "../../../Common/GridLayout.js" as GridLayout
.import "../../../Common/settings/SettingsSpec.js" as Spec
var OPTION_IDS = ["diskUsage", "brightnessSlider", "idleInhibitor", "user"];
var ACTION_IDS = ["settings", "lock", "power"];
var EDIT_ID = "edit";
var BUTTON_IDS = ACTION_IDS.concat([EDIT_ID]);
var FOOTER_CELLS = {
"runningApps": 4,
"user": 3,
@@ -30,13 +33,31 @@ function inFooter(widget) {
return !!widget?.footer;
}
function isRemovable(widget) {
return widget?.id !== EDIT_ID;
}
// A saved list without the pencil gets it back whatever state the migration left it in.
function ensureEditButton() {
const widgets = Common.SettingsData.controlCenterWidgets;
if (!Array.isArray(widgets) || widgets.some(widget => widget?.id === EDIT_ID))
return;
const fallback = Spec.SPEC.controlCenterWidgets.def.find(widget => widget.id === EDIT_ID);
Common.SettingsData.set("controlCenterWidgets", widgets.concat([Object.assign({}, fallback)]));
}
function footerMinCells(id) {
return isSliderWidget(id) ? 3 : 1;
}
function footerStep(value) {
const step = ControlCenter.CcMetrics.gridStep;
return Math.round(value / step) * step;
}
// `footerW` outlives the footer flag, so an item dragged back in returns at the width it left with.
function footerCells(widget) {
return Math.max(footerMinCells(widget?.id), Math.round(Number(widget?.footerW) || FOOTER_CELLS[widget?.id] || 1));
return Math.max(footerMinCells(widget?.id), footerStep(Number(widget?.footerW) || FOOTER_CELLS[widget?.id] || 1));
}
function footerFills(widget) {
@@ -47,17 +68,19 @@ function footerEnds(widget) {
return widget?.footerEnd === true;
}
function fitFooterCells(sizes, mins, capacity) {
function fitFooterCells(sizes, mins, capacity, keep, step) {
const fitted = sizes.slice();
let over = fitted.reduce((sum, cells) => sum + cells, 0) - capacity;
while (over > 0) {
const widest = fitted.reduce((best, cells, i) => cells > mins[i] && (best < 0 || cells > fitted[best]) ? i : best, -1);
if (widest < 0)
break;
fitted[widest]--;
over--;
fitted[widest] -= step;
over -= step;
}
for (let i = fitted.length - 1; over > 0 && i >= 0; i--) {
if (keep[i])
continue;
over -= fitted[i];
fitted[i] = 0;
}
@@ -115,7 +138,7 @@ function sizeSpec(widget, columns, rows = Infinity) {
};
if (widget?.id === "user")
spec.w = (Number.isFinite(columns) ? columns : ControlCenter.CcMetrics.defaultColumns) - ACTION_IDS.length;
if (ACTION_IDS.includes(widget?.id))
if (BUTTON_IDS.includes(widget?.id))
spec.w = 1;
return spec;
}
@@ -142,7 +165,7 @@ function clampSize(widget, columns, rows = Infinity) {
function addWidget(widgetId, columns) {
const widgets = Common.SettingsData.controlCenterWidgets.slice();
const widget = defaultWidget(widgetId, columns);
if (ACTION_IDS.includes(widgetId))
if (BUTTON_IDS.includes(widgetId))
widget.small = true;
if (widgetId === "diskUsage") {
@@ -166,7 +189,7 @@ function generateUniqueId() {
function removeWidget(index) {
const widgets = Common.SettingsData.controlCenterWidgets.slice();
if (index < 0 || index >= widgets.length)
if (index < 0 || index >= widgets.length || !isRemovable(widgets[index]))
return;
widgets.splice(index, 1);
Common.SettingsData.set("controlCenterWidgets", widgets);
@@ -204,7 +227,7 @@ function moveToFooter(index, beforeIndex, cells, end) {
}
// Hands spare cells to fill items, the earlier ones taking the remainder.
function spreadFooterFill(cells, fills, spare) {
function spreadFooterFill(cells, fills, spare, step) {
const count = fills.filter(Boolean).length;
if (count === 0 || spare <= 0)
return cells;
@@ -212,7 +235,7 @@ function spreadFooterFill(cells, fills, spare) {
return cells.map((size, i) => {
if (!fills[i] || size === 0)
return size;
const share = Math.ceil(left / count);
const share = Math.ceil(left / count / step) * step;
left -= share;
return size + Math.max(0, share);
});
@@ -254,5 +277,5 @@ function resetToDefault() {
}
function clearAll() {
Common.SettingsData.set("controlCenterWidgets", []);
Common.SettingsData.set("controlCenterWidgets", Common.SettingsData.controlCenterWidgets.filter(widget => !isRemovable(widget)));
}
+1 -1
View File
@@ -54,7 +54,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.swapInto(changed, pinnedCells, draggingSourceIndex, dragCell) : changed;
return swapDrags && dragCell && draggingSourceIndex >= 0 ? GridUtils.dropInto(changed, pinnedCells, draggingSourceIndex, dragCell) : changed;
}
signal layoutCommitted(var items)
+16
View File
@@ -80,3 +80,19 @@ test("panel step follows the pointer and holds on a pinned edge", () => {
assert.equal(grid.nearestStep(rightEdge, 8, 6, 30, rightEdge(8) + 50), 9);
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 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" }]);
});
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" }]);
});
@@ -30,3 +30,12 @@ test("header entries become a user row and lock, power and settings tiles", () =
const released = store.migrateToVersion({ configVersion: 18, controlCenterWidgets: [{ id: "wifi", width: 50 }] }, 35);
assert.deepEqual(JSON.parse(JSON.stringify(released.controlCenterWidgets.map(w => [w.id, w.w, w.h, !!w.footer]))), [["user", 5, 1, false], ["settings", 1, 1, false], ["lock", 1, 1, false], ["power", 1, 1, false], ["wifi", 4, 1, false], ["runningApps", 4, 1, true]]);
});
test("the edit button joins the footer end once and keeps a placed one", () => {
const migrated = store.migrateToVersion({ configVersion: 36, controlCenterWidgets: [{ id: "wifi", w: 4, h: 1 }] }, 37);
assert.equal(migrated.configVersion, 37);
assert.deepEqual(JSON.parse(JSON.stringify(migrated.controlCenterWidgets)), [{ id: "wifi", w: 4, h: 1 }, { id: "edit", enabled: true, w: 1, h: 1, small: true, footer: true, footerEnd: true }]);
const placed = store.migrateToVersion({ configVersion: 36, controlCenterWidgets: [{ id: "edit", w: 1, h: 1, col: 2, row: 0 }] }, 37);
assert.deepEqual(JSON.parse(JSON.stringify(placed.controlCenterWidgets)), [{ id: "edit", w: 1, h: 1, col: 2, row: 0 }]);
assert.equal(store.migrateToVersion({ configVersion: 36 }, 37).controlCenterWidgets, undefined);
});