mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-18 09:14:01 -04:00
bar: shrink flexible widgets before moving anything into overflow
This commit is contained in:
@@ -43,12 +43,23 @@ Item {
|
||||
for (let index = 0; index < (entryRepeater?.count ?? 0); index++) {
|
||||
const wrapper = entryRepeater.itemAt(index);
|
||||
entries.push({
|
||||
size: wrapper?.primarySize ?? 0,
|
||||
size: wrapper?.preferredSize ?? 0,
|
||||
min: wrapper?.minimumSize ?? 0,
|
||||
mode: wrapper?.overflowMode ?? "bar"
|
||||
});
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
readonly property var inlineEntries: {
|
||||
root.entryRevision;
|
||||
const entries = [];
|
||||
for (let index = 0; index < (entryRepeater?.count ?? 0); index++)
|
||||
entries.push({
|
||||
size: entryRepeater.itemAt(index)?.primarySize ?? 0
|
||||
});
|
||||
return entries;
|
||||
}
|
||||
readonly property var allottedSizes: barContent?.overflowPlan?.sizes?.[section] ?? []
|
||||
readonly property var hiddenEntries: {
|
||||
root.entryRevision;
|
||||
const entries = [];
|
||||
@@ -86,6 +97,7 @@ Item {
|
||||
}
|
||||
|
||||
onLayoutEntriesChanged: requestOverflowLayout()
|
||||
onInlineEntriesChanged: layoutRequested()
|
||||
onOverflowIndicesChanged: layoutRequested()
|
||||
onOverflowPositionChanged: requestOverflowLayout()
|
||||
onOverflowTriggerSizeChanged: requestOverflowLayout()
|
||||
@@ -97,7 +109,7 @@ Item {
|
||||
}
|
||||
|
||||
function inlineLayout() {
|
||||
return OverflowLayout.section(layoutEntries, hiddenEntries.map(entry => entry.occurrenceOrder), widgetSpacing, overflowTriggerSize, overflowPosition);
|
||||
return OverflowLayout.section(inlineEntries, hiddenEntries.map(entry => entry.occurrenceOrder), widgetSpacing, overflowTriggerSize, overflowPosition);
|
||||
}
|
||||
|
||||
Item {
|
||||
|
||||
@@ -79,6 +79,7 @@ Item {
|
||||
}, overflowPlan.hidden);
|
||||
const next = {
|
||||
hidden: result.hidden,
|
||||
sizes: result.sizes,
|
||||
fits: result.fits
|
||||
};
|
||||
if (JSON.stringify(next) !== JSON.stringify(overflowPlan))
|
||||
@@ -428,7 +429,6 @@ Item {
|
||||
surfaceContext: widgetContext
|
||||
spacingTight: topBarContent.spacingTight
|
||||
overlapping: topBarContent.overlapping
|
||||
leadingSectionLimit: topBarContent.fitToWidgets || topBarContent._barIsVertical || hCenterSection.contentSize <= 0 ? Infinity : hCenterSection.x + hCenterSection.contentStart - topBarContent.sectionGap - hLeftSection.x
|
||||
}
|
||||
readonly property var allComponents: widgetFactory.componentMap
|
||||
readonly property var componentMap: widgetFactory.componentMap
|
||||
|
||||
@@ -132,8 +132,97 @@ function measure(sections, hidden, options) {
|
||||
};
|
||||
}
|
||||
|
||||
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 names = ["left", "center", "right"];
|
||||
const hidden = {};
|
||||
for (const name of names) {
|
||||
hidden[name] = [];
|
||||
@@ -144,8 +233,9 @@ function resolve(sections, options, previous = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
let measured = measure(sections, hidden, options);
|
||||
while (!measured.fits) {
|
||||
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;
|
||||
@@ -169,15 +259,15 @@ function resolve(sections, options, previous = {}) {
|
||||
if (!candidate)
|
||||
break;
|
||||
hidden[candidate.name].push(candidate.index);
|
||||
measured = measure(sections, hidden, options);
|
||||
fit = shrinkToFit(sections, hidden, options);
|
||||
}
|
||||
|
||||
const fresh = {};
|
||||
for (const name of names)
|
||||
fresh[name] = hidden[name].slice();
|
||||
const freshFits = measured.fits;
|
||||
const freshFits = fit.measured.fits;
|
||||
|
||||
// Hysteresis: a restored widget must also fit the margin, or it would bounce in and out at the boundary.
|
||||
// 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] ?? []) {
|
||||
@@ -197,18 +287,22 @@ function resolve(sections, options, previous = {}) {
|
||||
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: entry.size + (options.restoreMargin ?? 0)
|
||||
size: floor,
|
||||
min: floor
|
||||
}) : entry)
|
||||
});
|
||||
if (measure(padded, trial, options).fits)
|
||||
if (shrinkToFit(padded, trial, options).measured.fits)
|
||||
hidden[name] = trial[name];
|
||||
}
|
||||
const result = freshFits && !measure(sections, hidden, options).fits ? fresh : hidden;
|
||||
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
|
||||
}, measure(sections, result, options));
|
||||
hidden: result,
|
||||
sizes: fit.sizes
|
||||
}, fit.measured);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,10 @@ Item {
|
||||
readonly property real itemHeight: widgetItem?.height ?? 0
|
||||
readonly property bool available: active && widgetLoader.widgetEnabled && itemVisible && effectiveVisible && itemWidth > 0 && itemHeight > 0
|
||||
readonly property real primarySize: available ? (sectionContext.isVertical ? itemHeight : itemWidth) : 0
|
||||
// A flexible widget reports the width it wants and the width it can live with; the resolver allots between them.
|
||||
readonly property real preferredSize: !available ? 0 : widgetItem && "naturalPrimarySize" in widgetItem ? widgetItem.naturalPrimarySize : primarySize
|
||||
readonly property real minimumSize: available && widgetItem && "minimumPrimarySize" in widgetItem ? Math.min(preferredSize, widgetItem.minimumPrimarySize) : preferredSize
|
||||
readonly property real allottedSize: sectionContext.allottedSizes[occurrenceOrder] ?? 0
|
||||
readonly property string overflowMode: OverflowLayout.placement(itemData, sectionContext.overflowDefaultMode)
|
||||
readonly property bool inOverflow: overflowMode === "always" || sectionContext.overflowIndices.includes(occurrenceOrder)
|
||||
readonly property bool inPopup: inOverflow && sectionContext.overflowOpen && !!sectionContext.overflowParent
|
||||
@@ -61,6 +65,7 @@ Item {
|
||||
isTopBarEdge: !root.inOverflow && isInColumn && section === "left" && root.sectionContext.edgeIsScreenEdge
|
||||
isBottomBarEdge: !root.inOverflow && isInColumn && section === "right" && root.sectionContext.edgeIsScreenEdge
|
||||
crossEdgeExtension: !root.inOverflow && !isInColumn && section !== "center" ? root.sectionContext.crossEdgeExtension : 0
|
||||
allottedPrimarySize: root.inOverflow ? 0 : root.allottedSize
|
||||
overflowAnchor: root.inOverflow ? root.sectionContext.overflowButton : null
|
||||
overflowSurface: root.inOverflow ? root.sectionContext.overflowSurface : null
|
||||
}
|
||||
|
||||
@@ -28,7 +28,10 @@ BasePill {
|
||||
return 456;
|
||||
}
|
||||
}
|
||||
property int availableWidth: maxWidth
|
||||
property real allottedPrimarySize: 0
|
||||
property int availableWidth: allottedPrimarySize > 0 ? allottedPrimarySize : maxWidth
|
||||
readonly property real naturalPrimarySize: isVerticalOrientation ? height : !hasWindowsOnCurrentWorkspace ? 0 : Theme.snap(Math.min(contentItem?.naturalWidth ?? 0, maxWidth - horizontalPadding * 2) + horizontalPadding * 2, dpr)
|
||||
readonly property real minimumPrimarySize: isVerticalOrientation ? height : Math.min(naturalPrimarySize, Theme.snap((contentItem?.minimumWidth ?? 0) + horizontalPadding * 2, dpr))
|
||||
readonly property real effectiveHorizontalWidth: Math.max(0, Math.min(maxWidth, availableWidth))
|
||||
readonly property real effectiveHorizontalInnerWidth: Math.max(0, effectiveHorizontalWidth - horizontalPadding * 2)
|
||||
property var activeWindow: null
|
||||
@@ -114,6 +117,21 @@ BasePill {
|
||||
|
||||
content: Component {
|
||||
Item {
|
||||
readonly property bool iconShown: horizontalAppIcon.visible || horizontalSteamIcon.visible
|
||||
readonly property real appTextNatural: appText.visible ? Math.min(appText.implicitWidth, root.compactMode ? 80 : 180) : 0
|
||||
readonly property real naturalWidth: {
|
||||
const parts = [];
|
||||
if (iconShown)
|
||||
parts.push(contentRow.iconSize);
|
||||
if (appText.visible)
|
||||
parts.push(appTextNatural);
|
||||
if (appSeparator.visible)
|
||||
parts.push(appSeparator.width);
|
||||
if (titleText.visible)
|
||||
parts.push(titleText.implicitWidth);
|
||||
return parts.reduce((sum, part) => sum + part, 0) + Math.max(0, parts.length - 1) * contentRow.spacing;
|
||||
}
|
||||
readonly property real minimumWidth: (iconShown ? contentRow.iconSize + contentRow.spacing : 0) + Math.min(48, appText.visible ? appTextNatural : titleText.visible ? titleText.implicitWidth : 0)
|
||||
implicitWidth: {
|
||||
if (!root.hasWindowsOnCurrentWorkspace)
|
||||
return 0;
|
||||
|
||||
@@ -28,6 +28,9 @@ BasePill {
|
||||
property bool compactMode: false
|
||||
property var widgetData: null
|
||||
readonly property bool adaptiveWidthEnabled: SettingsData.widgetOption("music", widgetData, "mediaAdaptiveWidthEnabled")
|
||||
property real allottedPrimarySize: 0
|
||||
readonly property real naturalPrimarySize: isVerticalOrientation ? height : !playerAvailable ? 0 : Theme.snap((contentItem?.naturalContentWidth ?? 0) + horizontalPadding * 2, dpr)
|
||||
readonly property real minimumPrimarySize: isVerticalOrientation ? height : !playerAvailable ? 0 : Theme.snap((contentItem?.fixedContentWidth ?? 0) + horizontalPadding * 2, dpr)
|
||||
readonly property bool lyricsEnabled: SettingsData.widgetOption("music", widgetData, "mediaShowLyrics") && LyricsService.allowed
|
||||
readonly property bool coverArtEnabled: SettingsData.widgetOption("music", widgetData, "mediaShowCoverArt")
|
||||
readonly property string coverArtUrl: coverArtEnabled && activePlayer && TrackArtService.artReadyFor(activePlayer) ? TrackArtService.resolvedArtUrl : ""
|
||||
@@ -228,7 +231,7 @@ BasePill {
|
||||
content: Component {
|
||||
Item {
|
||||
id: contentRoot
|
||||
readonly property real measuredTextWidth: {
|
||||
readonly property real naturalTextWidth: {
|
||||
if (!root.playerAvailable || root.maxTextWidth <= 0)
|
||||
return 0;
|
||||
// Preserve the fixed-width text slot even if metadata is briefly empty.
|
||||
@@ -241,10 +244,11 @@ BasePill {
|
||||
return 0;
|
||||
return Math.min(root.maxTextWidth, Math.ceil(rawWidth));
|
||||
}
|
||||
readonly property int horizontalContentWidth: {
|
||||
const controlsWidth = 64 * root.contentScale + Theme.spacingXS * 2;
|
||||
return root.badgeExtent + controlsWidth + (measuredTextWidth > 0 ? measuredTextWidth + Theme.spacingXS : 0);
|
||||
}
|
||||
readonly property real fixedContentWidth: root.badgeExtent + 64 * root.contentScale + Theme.spacingXS * 2
|
||||
readonly property real textBudget: root.allottedPrimarySize > 0 ? Math.max(0, root.allottedPrimarySize - root.horizontalPadding * 2 - fixedContentWidth - Theme.spacingXS) : Infinity
|
||||
readonly property real measuredTextWidth: Math.min(naturalTextWidth, textBudget)
|
||||
readonly property real naturalContentWidth: fixedContentWidth + (naturalTextWidth > 0 ? naturalTextWidth + Theme.spacingXS : 0)
|
||||
readonly property int horizontalContentWidth: fixedContentWidth + (measuredTextWidth > 0 ? measuredTextWidth + Theme.spacingXS : 0)
|
||||
|
||||
implicitWidth: root.playerAvailable ? (root.isVerticalOrientation ? root.currentContentWidth : horizontalContentWidth) : 0
|
||||
implicitHeight: root.playerAvailable ? root.currentContentHeight : 0
|
||||
|
||||
@@ -43,6 +43,7 @@ Item {
|
||||
readonly property real visualWidth: Theme.snap(isVerticalOrientation ? widgetThickness : (contentLoader.item ? (contentLoader.item.implicitWidth + horizontalPadding * 2) : 0), dpr)
|
||||
readonly property real visualHeight: Theme.snap(isVerticalOrientation ? (contentLoader.item ? (contentLoader.item.implicitHeight + horizontalPadding * 2) : 0) : widgetThickness, dpr)
|
||||
readonly property alias visualContent: visualContent
|
||||
readonly property alias contentItem: contentLoader.item
|
||||
readonly property real barEdgeExtension: BarMetrics.fittsReach
|
||||
readonly property real gapExtension: sectionSpacing
|
||||
readonly property real leftMargin: !isVerticalOrientation ? (isLeftBarEdge && isFirst ? barEdgeExtension : (isFirst ? gapExtension : gapExtension / 2)) : 0
|
||||
|
||||
@@ -14,7 +14,6 @@ Item {
|
||||
readonly property string _barScreenName: surfaceContext.screen?.name ?? ""
|
||||
property bool spacingTight: false
|
||||
property bool overlapping: false
|
||||
property real leadingSectionLimit: Infinity
|
||||
function getWidgetSection(item) {
|
||||
for (let current = item; current; current = current.parent) {
|
||||
if (typeof current.section === "string")
|
||||
@@ -329,12 +328,6 @@ Item {
|
||||
FocusedApp {
|
||||
id: focusedWindowWidget
|
||||
axis: surfaceContext.axis
|
||||
availableWidth: {
|
||||
const container = focusedWindowWidget.parent?.parent;
|
||||
if (!container || !isFinite(root.leadingSectionLimit) || root.getWidgetSection(focusedWindowWidget) !== "left")
|
||||
return focusedWindowWidget.maxWidth;
|
||||
return Math.max(0, root.leadingSectionLimit - container.x);
|
||||
}
|
||||
widgetThickness: surfaceContext.widgetThickness
|
||||
barThickness: surfaceContext.thickness
|
||||
barSpacing: barConfig?.spacing ?? 4
|
||||
|
||||
@@ -10,6 +10,7 @@ Loader {
|
||||
property var overflowAnchor: null
|
||||
property var overflowSurface: null
|
||||
property bool live: true
|
||||
property real allottedPrimarySize: 0
|
||||
property string instanceId: widgetData?.id ?? widgetId
|
||||
readonly property string registrationId: (surfaceContext?.kind ?? "bar") + ":" + (surfaceContext?.configId ?? barConfig?.id ?? "") + ":" + section + ":" + instanceId
|
||||
property int occurrenceOrder: 0
|
||||
@@ -83,7 +84,7 @@ Loader {
|
||||
}
|
||||
|
||||
Instantiator {
|
||||
model: ["parentScreen", "section", "widgetThickness", "barThickness", "barSpacing", "barConfig", "blurBarWindow", "axis", "widgetData", "isFirst", "isLast", "sectionSpacing", "sectionAvailablePrimarySize", "isLeftBarEdge", "isRightBarEdge", "isTopBarEdge", "isBottomBarEdge", "crossEdgeExtension", "segmentRole", "surfaceContext", "hostContext"]
|
||||
model: ["parentScreen", "section", "widgetThickness", "barThickness", "barSpacing", "barConfig", "blurBarWindow", "axis", "widgetData", "isFirst", "isLast", "sectionSpacing", "sectionAvailablePrimarySize", "isLeftBarEdge", "isRightBarEdge", "isTopBarEdge", "isBottomBarEdge", "crossEdgeExtension", "segmentRole", "surfaceContext", "hostContext", "allottedPrimarySize"]
|
||||
delegate: Binding {
|
||||
required property string modelData
|
||||
target: root.item
|
||||
|
||||
@@ -69,3 +69,20 @@ test("a center holder does not replace the configured middle-widget anchor", ()
|
||||
assert.equal(clockStart + 40, 300);
|
||||
}
|
||||
});
|
||||
|
||||
test("flexible widgets shrink to their minimum before anything overflows", () => {
|
||||
const result = solve([entry(100), { size: 200, min: 60, mode: "auto" }], [entry(80)], [], { length: 500 });
|
||||
assert.deepEqual(plain(result.hidden), { left: [], center: [], right: [] });
|
||||
assert.deepEqual(plain(result.sizes.left), [100, 102]);
|
||||
assert.equal(result.fits, true);
|
||||
const tight = solve([entry(100), { size: 200, min: 60, mode: "auto" }], [entry(80)], [], { length: 400 });
|
||||
assert.deepEqual(plain(tight.hidden.left), [1]);
|
||||
});
|
||||
|
||||
test("a hidden flexible widget returns once its minimum fits the margin", () => {
|
||||
const sections = [[entry(100), { size: 200, min: 60, mode: "auto" }], [entry(80)], []];
|
||||
const result = solve(...sections, { length: 432 }, { left: [1] });
|
||||
assert.deepEqual(plain(result.hidden.left), []);
|
||||
assert.deepEqual(plain(result.sizes.left), [100, 68]);
|
||||
assert.deepEqual(plain(solve(...sections, { length: 420 }, { left: [1] }).hidden.left), [1]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user