Files
purian23 c0f08f08cb refactor(island/settings): update notch mode & radius defaults
- Merge island satellites and standard bar goth corner options in shared code
- Update bar settings to collapse inactive sliders
2026-10-08 03:44:42 -04:00

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);
}