mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-15 07:44:01 -04:00
shell: cut animation frame cost and cold open stalls dock: restore 1.6-like spacing behaviors fixes #3537
388 lines
15 KiB
JavaScript
388 lines
15 KiB
JavaScript
.pragma library
|
|
|
|
var EDGES = ["top", "bottom", "left", "right"];
|
|
|
|
function clone(value) {
|
|
return JSON.parse(JSON.stringify(value));
|
|
}
|
|
|
|
function create(id, name) {
|
|
return {
|
|
id: id,
|
|
name: name,
|
|
enabled: false,
|
|
screenPreferences: ["all"],
|
|
showOnLastDisplay: true,
|
|
position: 1,
|
|
mode: "compact",
|
|
taskbarAlign: "center",
|
|
widgetExpansion: "popout",
|
|
iconSize: 42,
|
|
spacing: 8,
|
|
itemSpacing: 8,
|
|
margin: 8,
|
|
bottomGap: 0,
|
|
transparency: 1,
|
|
followInterfaceStyle: true,
|
|
autoHide: false,
|
|
smartAutoHide: false,
|
|
useOverlayLayer: false,
|
|
editOnRightClick: false,
|
|
showOnFullscreen: false,
|
|
openOnOverview: false,
|
|
groupByApp: false,
|
|
separatePinnedAndRunningApps: false,
|
|
restoreSpecialWorkspaceOnClick: false,
|
|
isolateDisplays: false,
|
|
indicatorStyle: "circle",
|
|
borderEnabled: false,
|
|
borderColor: "surfaceText",
|
|
borderOpacity: 1,
|
|
borderThickness: 1,
|
|
launcherEnabled: true,
|
|
launcherLogoMode: "os",
|
|
launcherLogoCustomPath: "",
|
|
launcherLogoColorOverride: "",
|
|
launcherLogoSizeOffset: 0,
|
|
launcherLogoBrightness: 0.5,
|
|
launcherLogoContrast: 1,
|
|
maxVisibleApps: 0,
|
|
maxVisibleRunningApps: 0,
|
|
showOverflowBadge: true,
|
|
showTrash: false,
|
|
trashFileManager: "default",
|
|
trashCustomCommand: "",
|
|
order: [],
|
|
widgets: [
|
|
{
|
|
id: id + "_launcher",
|
|
widgetId: "dockLauncher",
|
|
enabled: true
|
|
},
|
|
{
|
|
id: id + "_apps",
|
|
widgetId: "appsDock",
|
|
enabled: true
|
|
},
|
|
{
|
|
id: id + "_trash",
|
|
widgetId: "dockTrash",
|
|
enabled: true
|
|
}
|
|
]
|
|
};
|
|
}
|
|
|
|
function bounded(value, fallback, minimum, maximum) {
|
|
const number = Number(value);
|
|
return Number.isFinite(number) ? Math.max(minimum, Math.min(maximum, number)) : fallback;
|
|
}
|
|
|
|
function normalize(configs) {
|
|
if (!Array.isArray(configs))
|
|
return [];
|
|
const ids = new Set();
|
|
return configs.filter(config => {
|
|
if (!config || typeof config.id !== "string" || !config.id || ids.has(config.id))
|
|
return false;
|
|
ids.add(config.id);
|
|
return true;
|
|
}).map(config => {
|
|
const defaults = create(config.id, config.id);
|
|
const result = Object.assign({}, defaults, clone(config));
|
|
result.name = typeof config.name === "string" && config.name.trim() ? config.name : config.id;
|
|
result.position = Number.isInteger(config.position) && config.position >= 0 && config.position < 4 ? config.position : 1;
|
|
result.mode = config.mode === "taskbar" ? "taskbar" : "compact";
|
|
result.taskbarAlign = ["start", "center", "end"].includes(config.taskbarAlign) ? config.taskbarAlign : "center";
|
|
result.widgetExpansion = config.widgetExpansion === "inline" ? "inline" : "popout";
|
|
result.iconSize = bounded(result.iconSize, defaults.iconSize, 16, 96);
|
|
result.itemSpacing = bounded(result.itemSpacing, defaults.itemSpacing, 0, 32);
|
|
for (const key of ["spacing", "margin"])
|
|
result[key] = bounded(result[key], defaults[key], 0, 128);
|
|
result.bottomGap = bounded(result.bottomGap, defaults.bottomGap, -128, 128);
|
|
for (const key of ["transparency", "borderOpacity", "launcherLogoBrightness"])
|
|
result[key] = bounded(result[key], 1, 0, 1);
|
|
for (const key of ["maxVisibleApps", "maxVisibleRunningApps"])
|
|
result[key] = Math.floor(bounded(result[key], 0, 0, 100));
|
|
for (const key of Object.keys(defaults)) {
|
|
if (typeof defaults[key] === "boolean")
|
|
result[key] = result[key] === true;
|
|
}
|
|
result.screenPreferences = Array.isArray(config.screenPreferences) ? config.screenPreferences.filter(pref => (typeof pref === "string" && pref) || (pref && typeof pref === "object" && (typeof pref.name === "string" || typeof pref.model === "string"))).map(pref => typeof pref === "string" ? pref : clone(pref)) : ["all"];
|
|
const widgetIds = new Set();
|
|
result.widgets = (Array.isArray(config.widgets) ? config.widgets : result.widgets).filter(widget => widget && (typeof widget === "string" || typeof widget.widgetId === "string" || typeof widget.id === "string")).map((widget, index) => {
|
|
const item = typeof widget === "string" ? {
|
|
widgetId: widget
|
|
} : clone(widget);
|
|
item.widgetId = item.widgetId || item.id;
|
|
let id = typeof item.id === "string" && item.id ? item.id : config.id + "_widget_" + index;
|
|
while (widgetIds.has(id))
|
|
id += "_";
|
|
item.id = id;
|
|
widgetIds.add(id);
|
|
item.enabled = item.enabled !== false;
|
|
return item;
|
|
});
|
|
result.widgets = withAppSlots(result.widgets, config.id);
|
|
result.order = Array.isArray(config.order) ? [...new Set(config.order.filter(id => typeof id === "string"))] : [];
|
|
delete result.launcherPosition;
|
|
delete result.thickness;
|
|
return result;
|
|
});
|
|
}
|
|
|
|
function matches(config, screen) {
|
|
return config.screenPreferences.some(pref => {
|
|
if (typeof pref === "string")
|
|
return pref === screen.name || !!(screen.model && pref === screen.model);
|
|
return pref.name === screen.name || !!(screen.model && pref.model === screen.model);
|
|
});
|
|
}
|
|
|
|
function pick(eligible, screen, screens, screenMatches) {
|
|
const explicit = eligible.find(config => (screenMatches || matches)(config, screen));
|
|
if (explicit)
|
|
return explicit;
|
|
const fallback = eligible.find(config => config.screenPreferences.includes("all"));
|
|
if (fallback)
|
|
return fallback;
|
|
if (screens?.length !== 1)
|
|
return null;
|
|
return eligible.find(config => config.showOnLastDisplay && config.screenPreferences.length > 0) || null;
|
|
}
|
|
|
|
// One dock may claim each edge of a screen, so resolution runs per edge rather than per screen.
|
|
// An enabled dock owns its edge; a dock shown only in the overview takes an edge nothing else holds.
|
|
function resolveEdge(configs, screen, screens, edge, screenMatches) {
|
|
if (!screen)
|
|
return null;
|
|
const onEdge = configs.filter(config => EDGES[config.position] === edge);
|
|
return pick(onEdge.filter(config => config.enabled), screen, screens, screenMatches) ?? pick(onEdge.filter(config => config.openOnOverview), screen, screens, screenMatches);
|
|
}
|
|
|
|
function resolveAll(configs, screen, screens, screenMatches) {
|
|
return EDGES.map(edge => resolveEdge(configs, screen, screens, edge, screenMatches)).filter(Boolean);
|
|
}
|
|
|
|
function resolve(configs, screen, screens, screenMatches) {
|
|
return resolveAll(configs, screen, screens, screenMatches)[0] ?? null;
|
|
}
|
|
|
|
// Options the redesign gave a new default. A settings.json written before it stored only
|
|
// changed values, so the old default is what the user is actually running today.
|
|
var LEGACY_DEFAULTS = {
|
|
iconSize: 40,
|
|
spacing: 4,
|
|
margin: 0,
|
|
bottomGap: 0,
|
|
launcherEnabled: false,
|
|
launcherLogoMode: "apps"
|
|
};
|
|
|
|
function migrate(settings) {
|
|
if (Array.isArray(settings.dockConfigs))
|
|
return normalize(settings.dockConfigs);
|
|
const config = Object.assign(create("dock", "Dock"), LEGACY_DEFAULTS);
|
|
config.enabled = settings.showDock === true;
|
|
const preferences = settings.screenPreferences?.dock;
|
|
config.screenPreferences = Array.isArray(preferences) && preferences.length ? clone(preferences) : ["all"];
|
|
for (const option of Object.keys(config)) {
|
|
const key = "dock" + option[0].toUpperCase() + option.slice(1);
|
|
if (settings[key] !== undefined)
|
|
config[option] = clone(settings[key]);
|
|
}
|
|
config.itemSpacing = Math.min(8, Math.max(4, config.iconSize * 0.08));
|
|
for (const key of ["transparency", "borderOpacity", "launcherLogoBrightness", "launcherLogoContrast"]) {
|
|
if (config[key] > 1)
|
|
config[key] /= 100;
|
|
}
|
|
config.showOnLastDisplay = settings.showOnLastDisplay?.dock ?? true;
|
|
return normalize([config]);
|
|
}
|
|
|
|
function move(items, from, to) {
|
|
if (from < 0 || from >= items.length || to < 0 || to >= items.length || from === to)
|
|
return items;
|
|
const result = items.slice();
|
|
result.splice(to, 0, result.splice(from, 1)[0]);
|
|
return result;
|
|
}
|
|
|
|
function allocation(sizes, flexible, available, spacing) {
|
|
const fixed = sizes.reduce((sum, size, i) => sum + (flexible[i] ? 0 : Math.max(0, size)), 0);
|
|
const gaps = Math.max(0, sizes.length - 1) * spacing;
|
|
const count = flexible.filter(Boolean).length;
|
|
const share = count ? Math.max(0, available - fixed - gaps) / count : 0;
|
|
return sizes.map((size, i) => flexible[i] ? share : Math.max(0, size));
|
|
}
|
|
|
|
var APP_SLOT_TYPES = {
|
|
dockLauncher: "launcher",
|
|
dockTrash: "trash"
|
|
};
|
|
|
|
function withAppSlots(widgets, id) {
|
|
const result = widgets.filter((widget, index) => !(widget.widgetId in APP_SLOT_TYPES) || widgets.findIndex(other => other.widgetId === widget.widgetId) === index);
|
|
const apps = () => result.findIndex(widget => widget.widgetId === "appsDock");
|
|
if (!result.some(widget => widget.widgetId === "dockLauncher"))
|
|
result.splice(Math.max(0, apps()), 0, {
|
|
id: id + "_launcher",
|
|
widgetId: "dockLauncher",
|
|
enabled: true
|
|
});
|
|
if (!result.some(widget => widget.widgetId === "dockTrash"))
|
|
result.splice(apps() < 0 ? result.length : apps() + 1, 0, {
|
|
id: id + "_trash",
|
|
widgetId: "dockTrash",
|
|
enabled: true
|
|
});
|
|
return result;
|
|
}
|
|
|
|
function isPinnedApp(app) {
|
|
return app.isPinned === true && app.type !== "launcher" && typeof app.appId === "string" && app.appId !== "";
|
|
}
|
|
|
|
function pinUnit(app) {
|
|
return "pin:" + app.appId;
|
|
}
|
|
|
|
function pinUnits(apps) {
|
|
return apps.filter(isPinnedApp).map(pinUnit);
|
|
}
|
|
|
|
// The saved order places widgets and pinned apps; the apps widget stands for the running windows,
|
|
// which keep the compositor's order. Pin slots are refilled in pin order so a plain pin drag still wins.
|
|
function unitList(widgets, pins, order) {
|
|
const widgetIds = widgets.map(widget => widget.id);
|
|
const known = new Set(widgetIds.concat(pins));
|
|
const result = [];
|
|
for (const id of order || []) {
|
|
if (known.has(id) && !result.includes(id))
|
|
result.push(id);
|
|
}
|
|
for (const id of widgetIds) {
|
|
if (!result.includes(id))
|
|
result.push(id);
|
|
}
|
|
const remaining = pins.slice();
|
|
for (let i = 0; i < result.length; i++) {
|
|
if (result[i].startsWith("pin:"))
|
|
result[i] = remaining.shift();
|
|
}
|
|
if (remaining.length === 0)
|
|
return result;
|
|
const lastPin = result.map(id => id.startsWith("pin:")).lastIndexOf(true);
|
|
const apps = result.indexOf(widgets.find(widget => widget.widgetId === "appsDock")?.id);
|
|
result.splice(lastPin >= 0 ? lastPin + 1 : apps >= 0 ? apps : result.length, 0, ...remaining);
|
|
return result;
|
|
}
|
|
|
|
function surfaceItems(widgets, apps, order) {
|
|
const items = [];
|
|
const byId = new Map(widgets.map(widget => [widget.id, widget]));
|
|
const appsWidget = widgets.find(widget => widget.widgetId === "appsDock" && widget.enabled !== false);
|
|
const slotTypes = Object.values(APP_SLOT_TYPES);
|
|
const push = (widget, unitId, app, index) => items.push({
|
|
id: widget.id + ":" + app.uniqueKey,
|
|
unitId,
|
|
widgetId: "application",
|
|
appData: app,
|
|
appIndex: index
|
|
});
|
|
for (const id of unitList(widgets, pinUnits(apps), order)) {
|
|
if (id.startsWith("pin:")) {
|
|
if (!appsWidget)
|
|
continue;
|
|
apps.forEach((app, index) => {
|
|
if (isPinnedApp(app) && pinUnit(app) === id && !app.isInOverflow)
|
|
push(appsWidget, id, app, index);
|
|
});
|
|
continue;
|
|
}
|
|
const widget = byId.get(id);
|
|
if (!widget || widget.enabled === false)
|
|
continue;
|
|
const slotType = APP_SLOT_TYPES[widget.widgetId];
|
|
if (!slotType && widget.widgetId !== "appsDock") {
|
|
items.push(widget);
|
|
continue;
|
|
}
|
|
apps.forEach((app, index) => {
|
|
if (slotType) {
|
|
if (app.type === slotType)
|
|
push(widget, widget.id, app, index);
|
|
return;
|
|
}
|
|
if (app.isInOverflow || isPinnedApp(app) || slotTypes.includes(app.type))
|
|
return;
|
|
if (app.type === "separator" && !isPinnedApp(items[items.length - 1]?.appData ?? {}))
|
|
return;
|
|
push(widget, widget.id, app, index);
|
|
});
|
|
}
|
|
return items;
|
|
}
|
|
|
|
// Maps the pin units of a new order back onto the raw pin list, which the strip keeps in the same sequence.
|
|
function reorderPins(pins, apps, order) {
|
|
const pinned = apps.filter(isPinnedApp);
|
|
if (pinned.length !== pins.length)
|
|
return null;
|
|
const next = order.filter(id => id.startsWith("pin:")).map(id => pins[pinned.findIndex(app => pinUnit(app) === id)]).filter(pin => pin !== undefined);
|
|
if (next.length !== pins.length || next.every((pin, index) => pin === pins[index]))
|
|
return null;
|
|
return next;
|
|
}
|
|
|
|
function unitOf(item) {
|
|
return item.unitId ?? item.id;
|
|
}
|
|
|
|
function units(items) {
|
|
const ids = [];
|
|
for (const item of items) {
|
|
const id = unitOf(item);
|
|
if (ids[ids.length - 1] !== id)
|
|
ids.push(id);
|
|
}
|
|
return ids;
|
|
}
|
|
|
|
function unitOrder(items, from, to) {
|
|
const ids = units(items);
|
|
const order = move(ids, ids.indexOf(from), ids.indexOf(to));
|
|
const result = [];
|
|
for (const id of order) {
|
|
items.forEach((item, index) => {
|
|
if (unitOf(item) === id)
|
|
result.push(index);
|
|
});
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// The leading edge decides, so a wide unit swaps with a narrow neighbour at the dock's end.
|
|
function unitTarget(spans, id, offset) {
|
|
const from = spans.findIndex(span => span.id === id);
|
|
if (from < 0)
|
|
return id;
|
|
const middle = span => (span.start + span.end) / 2;
|
|
let target = from;
|
|
for (let i = from + 1; i < spans.length && spans[from].end + offset > middle(spans[i]); i++)
|
|
target = i;
|
|
for (let i = from - 1; i >= 0 && spans[from].start + offset < middle(spans[i]); i--)
|
|
target = i;
|
|
return spans[target].id;
|
|
}
|
|
|
|
// Lane kept clear beside icons for running indicators.
|
|
function indicatorLane(config) {
|
|
return Math.max(8, Math.round(config.iconSize * 0.18));
|
|
}
|
|
|
|
// Thickness is derived, never set: content decides it and padding surrounds it.
|
|
function effectiveThickness(config) {
|
|
return config.iconSize + config.spacing * 2;
|
|
}
|