mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-14 15:24:00 -04:00
- Merge island satellites and standard bar goth corner options in shared code - Update bar settings to collapse inactive sliders
310 lines
11 KiB
JavaScript
310 lines
11 KiB
JavaScript
.pragma library
|
|
.import "CenterLayout.js" as CenterLayout
|
|
|
|
function pinned(widgetId) {
|
|
return ["spacer", "separator", "island"].includes(widgetId);
|
|
}
|
|
|
|
function placement(entry, fallback = "bar") {
|
|
if (pinned(entry?.widgetId))
|
|
return "bar";
|
|
const mode = entry?.overflowMode === "section" ? fallback : (entry?.overflowMode ?? fallback);
|
|
return mode === "auto" || mode === "always" ? mode : "bar";
|
|
}
|
|
|
|
function defaultPosition(name, count) {
|
|
return name === "right" ? 0 : count;
|
|
}
|
|
|
|
function expanderIcon(edge, open) {
|
|
switch (edge) {
|
|
case "left":
|
|
return open ? "keyboard_arrow_left" : "keyboard_arrow_right";
|
|
case "right":
|
|
return open ? "keyboard_arrow_right" : "keyboard_arrow_left";
|
|
case "bottom":
|
|
return open ? "keyboard_arrow_down" : "keyboard_arrow_up";
|
|
default:
|
|
return open ? "keyboard_arrow_up" : "keyboard_arrow_down";
|
|
}
|
|
}
|
|
|
|
function section(entries, hidden, spacing, triggerSize, triggerIndex) {
|
|
const sizes = [];
|
|
const indices = [];
|
|
const hasOverflow = hidden.some(index => entries[index]?.size > 0);
|
|
const insertion = Math.max(0, Math.min(entries.length, triggerIndex));
|
|
for (let index = 0; index <= entries.length; index++) {
|
|
if (index === insertion && hasOverflow) {
|
|
sizes.push(triggerSize);
|
|
indices.push(-1);
|
|
}
|
|
if (index === entries.length)
|
|
break;
|
|
sizes.push(entries[index]?.size > 0 && !hidden.includes(index) ? entries[index].size : null);
|
|
indices.push(index);
|
|
}
|
|
const layout = CenterLayout.resolve(sizes, 0, spacing, "geometric");
|
|
const positions = entries.map(() => null);
|
|
let buttonPosition = null;
|
|
const start = -layout.totalSize / 2;
|
|
for (let index = 0; index < indices.length; index++) {
|
|
const position = layout.positions[index] === null ? null : layout.positions[index] - start;
|
|
if (indices[index] === -1)
|
|
buttonPosition = position;
|
|
else
|
|
positions[indices[index]] = position;
|
|
}
|
|
const middle = Math.floor(entries.length / 2);
|
|
const anchor = {
|
|
count: entries.length,
|
|
middle: indices.indexOf(middle),
|
|
previous: indices.indexOf(middle - 1)
|
|
};
|
|
return {
|
|
sizes,
|
|
indices,
|
|
positions,
|
|
buttonPosition,
|
|
totalSize: layout.totalSize,
|
|
anchor
|
|
};
|
|
}
|
|
|
|
function measure(sections, hidden, options) {
|
|
const names = ["left", "center", "right"];
|
|
const layouts = {};
|
|
for (const name of names) {
|
|
layouts[name] = section(sections[name], hidden[name], options.spacing, options.triggerSize, options.positions?.[name] ?? defaultPosition(name, sections[name].length));
|
|
}
|
|
const min = options.start ?? 0;
|
|
const max = options.length - (options.end ?? 0);
|
|
const gap = options.spacing;
|
|
const leading = layouts.left.totalSize;
|
|
const trailing = layouts.right.totalSize;
|
|
const center = layouts.center;
|
|
const bounds = options.confineCenter ? {
|
|
min: min + (leading > 0 ? leading + gap : 0),
|
|
max: max - (trailing > 0 ? trailing + gap : 0)
|
|
} : null;
|
|
const centered = CenterLayout.resolve(center.sizes, options.length, gap, options.centeringMode ?? "geometric", bounds, center.anchor);
|
|
const livePositions = centered.positions.filter(position => position !== null);
|
|
const centerStart = livePositions.length ? Math.min(...livePositions) : 0;
|
|
const intervals = {
|
|
left: {
|
|
start: min,
|
|
end: min + leading,
|
|
size: leading
|
|
},
|
|
center: {
|
|
start: centerStart,
|
|
end: centerStart + centered.totalSize,
|
|
size: centered.totalSize
|
|
},
|
|
right: {
|
|
start: max - trailing,
|
|
end: max,
|
|
size: trailing
|
|
}
|
|
};
|
|
const collisions = new Set();
|
|
const reserve = options.reserve;
|
|
for (const name of names) {
|
|
const interval = intervals[name];
|
|
if (interval.size > 0 && (interval.start < min || interval.end > max || (reserve && interval.start < reserve.end && interval.end > reserve.start)))
|
|
collisions.add(name);
|
|
}
|
|
const live = names.filter(name => intervals[name].size > 0);
|
|
for (let index = 0; index < live.length; index++) {
|
|
for (let other = index + 1; other < live.length; other++) {
|
|
const a = live[index];
|
|
const b = live[other];
|
|
if (intervals[a].end + gap > intervals[b].start) {
|
|
collisions.add(a);
|
|
collisions.add(b);
|
|
}
|
|
}
|
|
}
|
|
return {
|
|
fits: collisions.size === 0,
|
|
collisions: [...collisions],
|
|
layouts,
|
|
intervals
|
|
};
|
|
}
|
|
|
|
const names = ["left", "center", "right"];
|
|
|
|
function minimumOf(entry) {
|
|
return typeof entry.min === "number" ? Math.min(entry.min, entry.size) : entry.size;
|
|
}
|
|
|
|
function preferredSizes(sections) {
|
|
const sizes = {};
|
|
for (const name of names)
|
|
sizes[name] = sections[name].map(entry => entry.size);
|
|
return sizes;
|
|
}
|
|
|
|
function sized(sections, sizes) {
|
|
const result = {};
|
|
for (const name of names)
|
|
result[name] = sections[name].map((entry, index) => Object.assign({}, entry, {
|
|
size: sizes[name][index]
|
|
}));
|
|
return result;
|
|
}
|
|
|
|
function flexibleEntries(sections, hidden, scope) {
|
|
const entries = [];
|
|
for (const name of scope) {
|
|
for (let index = 0; index < sections[name].length; index++) {
|
|
const entry = sections[name][index];
|
|
if (entry.size > 0 && minimumOf(entry) < entry.size && !hidden[name].includes(index))
|
|
entries.push({
|
|
name,
|
|
index
|
|
});
|
|
}
|
|
}
|
|
return entries;
|
|
}
|
|
|
|
// Flexible widgets give up width down to their minimum before anything leaves the bar; the colliding sections shrink first.
|
|
function shrinkToFit(sections, hidden, options) {
|
|
const preferred = preferredSizes(sections);
|
|
let measured = measure(sections, hidden, options);
|
|
if (measured.fits)
|
|
return {
|
|
sizes: preferred,
|
|
measured
|
|
};
|
|
let floor = null;
|
|
for (const scope of [measured.collisions, names]) {
|
|
const candidates = flexibleEntries(sections, hidden, scope);
|
|
if (!candidates.length)
|
|
continue;
|
|
const at = t => {
|
|
const sizes = preferredSizes(sections);
|
|
for (const {
|
|
name,
|
|
index
|
|
} of candidates) {
|
|
const entry = sections[name][index];
|
|
const minimum = minimumOf(entry);
|
|
sizes[name][index] = Math.floor(minimum + (entry.size - minimum) * t);
|
|
}
|
|
return {
|
|
sizes,
|
|
measured: measure(sized(sections, sizes), hidden, options)
|
|
};
|
|
};
|
|
floor = at(0);
|
|
if (!floor.measured.fits)
|
|
continue;
|
|
let low = 0;
|
|
let high = 1;
|
|
let best = floor;
|
|
for (let step = 0; step < 12; step++) {
|
|
const mid = (low + high) / 2;
|
|
const trial = at(mid);
|
|
if (trial.measured.fits) {
|
|
best = trial;
|
|
low = mid;
|
|
} else {
|
|
high = mid;
|
|
}
|
|
}
|
|
return best;
|
|
}
|
|
return floor ?? {
|
|
sizes: preferred,
|
|
measured
|
|
};
|
|
}
|
|
|
|
function resolve(sections, options, previous = {}) {
|
|
const hidden = {};
|
|
for (const name of names) {
|
|
hidden[name] = [];
|
|
for (let index = 0; index < sections[name].length; index++) {
|
|
const entry = sections[name][index];
|
|
if (entry.size > 0 && entry.mode === "always")
|
|
hidden[name].push(index);
|
|
}
|
|
}
|
|
|
|
let fit = shrinkToFit(sections, hidden, options);
|
|
while (!fit.measured.fits) {
|
|
const measured = fit.measured;
|
|
// The center anchors the bar, so the sides yield first; between them the longer one gives up a widget.
|
|
const colliding = measured.collisions.slice().sort((a, b) => (a === "center") - (b === "center") || measured.intervals[b].size - measured.intervals[a].size);
|
|
let candidate = null;
|
|
for (const name of colliding) {
|
|
const position = options.positions?.[name] ?? defaultPosition(name, sections[name].length);
|
|
const eligible = [];
|
|
for (let index = 0; index < sections[name].length; index++) {
|
|
const entry = sections[name][index];
|
|
if (entry.size > 0 && entry.mode === "auto" && !hidden[name].includes(index))
|
|
eligible.push(index);
|
|
}
|
|
eligible.sort((a, b) => Math.abs(a + 0.5 - position) - Math.abs(b + 0.5 - position));
|
|
if (eligible.length) {
|
|
candidate = {
|
|
name,
|
|
index: eligible[0]
|
|
};
|
|
break;
|
|
}
|
|
}
|
|
if (!candidate)
|
|
break;
|
|
hidden[candidate.name].push(candidate.index);
|
|
fit = shrinkToFit(sections, hidden, options);
|
|
}
|
|
|
|
const fresh = {};
|
|
for (const name of names)
|
|
fresh[name] = hidden[name].slice();
|
|
const freshFits = fit.measured.fits;
|
|
|
|
// Hysteresis: a restored widget must also fit the margin at its minimum, or it would bounce in and out at the boundary.
|
|
const restoring = [];
|
|
for (const name of names) {
|
|
for (const index of previous[name] ?? []) {
|
|
if (sections[name][index]?.size > 0 && sections[name][index].mode === "auto" && !hidden[name].includes(index)) {
|
|
hidden[name].push(index);
|
|
restoring.push({
|
|
name,
|
|
index
|
|
});
|
|
}
|
|
}
|
|
}
|
|
for (const {
|
|
name,
|
|
index
|
|
} of restoring) {
|
|
const trial = {};
|
|
for (const key of names)
|
|
trial[key] = hidden[key].filter(value => key !== name || value !== index);
|
|
const floor = minimumOf(sections[name][index]) + (options.restoreMargin ?? 0);
|
|
const padded = Object.assign({}, sections, {
|
|
[name]: sections[name].map((entry, entryIndex) => entryIndex === index ? Object.assign({}, entry, {
|
|
size: floor,
|
|
min: floor
|
|
}) : entry)
|
|
});
|
|
if (shrinkToFit(padded, trial, options).measured.fits)
|
|
hidden[name] = trial[name];
|
|
}
|
|
const result = freshFits && !shrinkToFit(sections, hidden, options).measured.fits ? fresh : hidden;
|
|
for (const name of names)
|
|
result[name].sort((a, b) => a - b);
|
|
fit = shrinkToFit(sections, result, options);
|
|
return Object.assign({
|
|
hidden: result,
|
|
sizes: fit.sizes
|
|
}, fit.measured);
|
|
}
|