From a3f1a3d62027f0461a9dce76fe59138f21f8e90a Mon Sep 17 00:00:00 2001 From: bbedward Date: Mon, 21 Sep 2026 13:27:14 -0400 Subject: [PATCH] feat(bar): add support for fractional sizes, fit to contents --- quickshell/Common/ShellLayout.qml | 8 +- quickshell/Common/Theme.qml | 8 +- quickshell/Common/settings/SettingsSpec.js | 2 + quickshell/Modules/DankBar/BarSurface.qml | 56 +-- quickshell/Modules/DankBar/CenterLayout.js | 24 +- quickshell/Modules/DankBar/CenterSection.qml | 5 +- quickshell/Modules/DankBar/DankBarBody.qml | 65 +++- quickshell/Modules/DankBar/DankBarContent.qml | 63 +++- quickshell/Modules/DankBar/SideSection.qml | 6 +- .../Modules/Plugins/PluginComponent.qml | 1 + .../Modules/Settings/DankBarAppearanceTab.qml | 38 ++- .../ColorDemoPlugin/ColorDemoWidget.qml | 4 +- .../DetailExampleWidget.qml | 4 +- .../ControlCenterExampleWidget.qml | 19 +- .../DashCounterExample/CounterWidget.qml | 1 - .../CompositeBarWidget.qml | 4 +- .../ExampleEmojiPlugin/EmojiWidget.qml | 4 +- .../StartupCheckWidget.qml | 2 +- .../ExampleWithVariants/VariantWidget.qml | 8 +- .../PopoutControlWidget.qml | 8 +- quickshell/tests/bar-content.test.mjs | 13 + quickshell/tests/qml/bar-fit.qml | 321 ++++++++++++++++++ quickshell/tests/run-qml.py | 2 +- .../translations/settings_search_index.json | 47 +++ 24 files changed, 623 insertions(+), 90 deletions(-) create mode 100644 quickshell/tests/qml/bar-fit.qml diff --git a/quickshell/Common/ShellLayout.qml b/quickshell/Common/ShellLayout.qml index d62c56feb..02df95079 100644 --- a/quickshell/Common/ShellLayout.qml +++ b/quickshell/Common/ShellLayout.qml @@ -22,7 +22,7 @@ Singleton { readonly property var layouts: screens.map(screen => Resolver.resolveScreen(SettingsData.barConfigs.map(config => ({ config, barThickness: Theme.barThickness(config.innerPadding ?? 4, screen.scale), - wingSize: config.gothCornersEnabled ? Math.max(0, config.gothCornerRadiusOverride ? config.gothCornerRadiusValue ?? 12 : Theme.windowRadius) : 0, + wingSize: config.gothCornersEnabled && root.barSpansEdge(config) ? Math.max(0, config.gothCornerRadiusOverride ? config.gothCornerRadiusValue ?? 12 : Theme.windowRadius) : 0, popupThickness: Theme.barThickness(Resolver.option(config, "innerPadding", root.primaryBar, 4), screen.scale), islandThickness: Resolver.islandThickness(config, SettingsData.islandDefaults), islandFloating: SettingsData.islandSetting(config, "islandFloating") @@ -37,6 +37,10 @@ Singleton { })) readonly property var islandKeys: layouts.reduce((keys, layout) => keys.concat(layout.instances.filter(instance => instance.kind === "island").map(instance => instance.key)), []) + function barSpansEdge(config) { + return (config?.barLengthMode ?? "full") === "full" && (config?.barLengthPadding ?? 0) <= 0; + } + function forScreen(screen) { const name = typeof screen === "string" ? screen : screen?.name; return layouts.find(layout => layout.screen.name === name) ?? null; @@ -118,7 +122,7 @@ Singleton { function barBounds(screen, thickness, position, config) { const appearance = config ?? primaryBar; const radius = (appearance?.gothCornerRadiusOverride ?? false) ? (appearance?.gothCornerRadiusValue ?? 12) : Theme.windowRadius; - const wing = (appearance?.gothCornersEnabled ?? false) ? Math.max(0, radius) : 0; + const wing = (appearance?.gothCornersEnabled ?? false) && barSpansEdge(appearance) ? Math.max(0, radius) : 0; const edge = position === undefined ? (primaryBar?.position ?? 0) : position; return Resolver.barBounds(forScreen(screen), thickness, edge, config, primaryBar, SettingsData.connectedFrameModeActive, wing); } diff --git a/quickshell/Common/Theme.qml b/quickshell/Common/Theme.qml index 4ae1ed2fb..85d82f0ab 100644 --- a/quickshell/Common/Theme.qml +++ b/quickshell/Common/Theme.qml @@ -1653,12 +1653,12 @@ Singleton { function barTextSize(barThickness, fontScale, maximizeText) { const scale = barThickness / 48; const dankBarScale = fontScale !== undefined ? fontScale : 1.0; - const maxScale = (maximizeText ?? false) ? 1.5 : 1.0; + const maximized = maximizeText ?? false; if (scale <= 0.75) - return Math.round(fontSizeSmall * 0.9 * dankBarScale * maxScale); + return Math.round((maximized ? fontSizeMedium : fontSizeSmall * 0.9) * dankBarScale); if (scale >= 1.25) - return Math.round(fontSizeMedium * dankBarScale * maxScale); - return Math.round(fontSizeSmall * dankBarScale * maxScale); + return Math.round((maximized ? fontSizeXLarge : fontSizeMedium) * dankBarScale); + return Math.round((maximized ? fontSizeLarge : fontSizeSmall) * dankBarScale); } // !TODO: plugin API only (dms-plugins DankKDEConnect); fold into a parametrized BatteryService ladder and drop from Theme diff --git a/quickshell/Common/settings/SettingsSpec.js b/quickshell/Common/settings/SettingsSpec.js index 9549225db..cec19ba12 100644 --- a/quickshell/Common/settings/SettingsSpec.js +++ b/quickshell/Common/settings/SettingsSpec.js @@ -1294,6 +1294,8 @@ var LOCAL_SPEC = { innerPadding: 4, barInsetPadding: -1, barLengthPadding: 0, + barLengthMode: "full", + barLengthPercent: 80, bottomGap: 0, attachToScreenEdge: false, followInterfaceStyle: true, diff --git a/quickshell/Modules/DankBar/BarSurface.qml b/quickshell/Modules/DankBar/BarSurface.qml index 61e7a2f2f..8126c96eb 100644 --- a/quickshell/Modules/DankBar/BarSurface.qml +++ b/quickshell/Modules/DankBar/BarSurface.qml @@ -19,7 +19,10 @@ Item { readonly property bool isRight: barPos === SettingsData.Position.Right readonly property bool farEdge: isBottom || isRight readonly property bool edgeAttached: (barConfig?.attachToScreenEdge ?? false) && !frameShapesBar + readonly property bool alongWings: !(barWindow.spansEdge ?? true) readonly property real wing: gothEnabled ? barWindow._wingR : 0 + readonly property real alongWing: alongWings ? wing : 0 + readonly property real crossWing: alongWings ? 0 : wing readonly property real rt: { if (frameShapesBar) return SettingsData.frameRounding; @@ -89,23 +92,38 @@ Item { readonly property real attachedRadius: edgeAttached || frameShapesBar ? 0 : rt readonly property real wingRootRadius: gothEnabled ? 0 : rt + readonly property real attachedCornerRadius: alongWings && gothEnabled ? 0 : attachedRadius + readonly property real freeCornerRadius: alongWings ? rt : wingRootRadius readonly property var shapeTarget: ({ - width: Math.max(0, axis.isVertical ? width - wing : width), - height: Math.max(0, axis.isVertical ? height : height - wing), + width: Math.max(0, axis.isVertical ? width - crossWing : width - alongWing * 2), + height: Math.max(0, axis.isVertical ? height - alongWing * 2 : height - crossWing), offsetAlong: wing, - offsetCross: farEdge ? wing : 0, - topLeftRadius: isTop || isLeft ? attachedRadius : wingRootRadius, - topRightRadius: isTop || isRight ? attachedRadius : wingRootRadius, - bottomLeftRadius: isBottom || isLeft ? attachedRadius : wingRootRadius, - bottomRightRadius: isBottom || isRight ? attachedRadius : wingRootRadius + offsetCross: farEdge ? crossWing : 0, + topLeftRadius: isTop || isLeft ? attachedCornerRadius : freeCornerRadius, + topRightRadius: isTop || isRight ? attachedCornerRadius : freeCornerRadius, + bottomLeftRadius: isBottom || isLeft ? attachedCornerRadius : freeCornerRadius, + bottomRightRadius: isBottom || isRight ? attachedCornerRadius : freeCornerRadius }) visible: !frameShapesBar anchors.fill: parent - anchors.leftMargin: -(gothEnabled && isRight ? barWindow._wingR : 0) - anchors.rightMargin: -(gothEnabled && isLeft ? barWindow._wingR : 0) - anchors.topMargin: -(gothEnabled && isBottom ? barWindow._wingR : 0) - anchors.bottomMargin: -(gothEnabled && isTop ? barWindow._wingR : 0) + anchors.leftMargin: -(axis.isVertical ? (isRight ? crossWing : 0) : alongWing) + anchors.rightMargin: -(axis.isVertical ? (isLeft ? crossWing : 0) : alongWing) + anchors.topMargin: -(axis.isVertical ? alongWing : (isBottom ? crossWing : 0)) + anchors.bottomMargin: -(axis.isVertical ? alongWing : (isTop ? crossWing : 0)) + + function alongWingCorner(leading) { + switch (barPos) { + case SettingsData.Position.Bottom: + return leading ? "topLeft" : "topRight"; + case SettingsData.Position.Left: + return leading ? "topRight" : "bottomRight"; + case SettingsData.Position.Right: + return leading ? "topLeft" : "bottomLeft"; + default: + return leading ? "bottomLeft" : "bottomRight"; + } + } function applyTarget() { if (width <= 0 || height <= 0 || !visible) { @@ -175,8 +193,8 @@ Item { MorphSurface { id: body motion: motion - x: root.axis.isVertical ? Math.max(0, motion.currentOffsetCross) : 0 - y: root.axis.isVertical ? 0 : Math.max(0, motion.currentOffsetCross) + x: Math.max(0, root.axis.isVertical ? motion.currentOffsetCross : (root.alongWings ? motion.currentOffsetAlong : 0)) + y: Math.max(0, root.axis.isVertical ? (root.alongWings ? motion.currentOffsetAlong : 0) : motion.currentOffsetCross) color: root.barWindow._bgColor } @@ -185,9 +203,9 @@ Item { radius: Math.max(0, motion.currentOffsetAlong) color: root.barWindow._bgColor visible: root.gothEnabled && radius > 0 - x: root.isLeft ? body.width : 0 - y: root.isTop ? body.height : 0 - corner: root.isTop ? "bottomRight" : root.isBottom ? "topRight" : root.isLeft ? "bottomRight" : "bottomLeft" + x: root.alongWings ? (root.axis.isVertical && root.isRight ? root.width - radius : 0) : (root.isLeft ? body.width : 0) + y: root.alongWings ? (!root.axis.isVertical && root.isBottom ? root.height - radius : 0) : (root.isTop ? body.height : 0) + corner: root.alongWings ? root.alongWingCorner(true) : root.isTop ? "bottomRight" : root.isBottom ? "topRight" : root.isLeft ? "bottomRight" : "bottomLeft" } GothCorner { @@ -195,9 +213,9 @@ Item { radius: Math.max(0, motion.currentOffsetAlong) color: root.barWindow._bgColor visible: root.gothEnabled && radius > 0 - x: root.axis.isVertical ? (root.isLeft ? body.width : 0) : root.width - radius - y: root.axis.isVertical ? root.height - radius : (root.isTop ? body.height : 0) - corner: root.isTop ? "bottomLeft" : root.isBottom ? "topLeft" : root.isLeft ? "topRight" : "topLeft" + x: root.axis.isVertical ? (root.alongWings ? (root.isRight ? root.width - radius : 0) : (root.isLeft ? body.width : 0)) : root.width - radius + y: root.axis.isVertical ? root.height - radius : (root.alongWings ? (root.isBottom ? root.height - radius : 0) : (root.isTop ? body.height : 0)) + corner: root.alongWings ? root.alongWingCorner(false) : root.isTop ? "bottomLeft" : root.isBottom ? "topLeft" : root.isLeft ? "topRight" : "topLeft" } Rectangle { diff --git a/quickshell/Modules/DankBar/CenterLayout.js b/quickshell/Modules/DankBar/CenterLayout.js index 17555b38a..a7031e23a 100644 --- a/quickshell/Modules/DankBar/CenterLayout.js +++ b/quickshell/Modules/DankBar/CenterLayout.js @@ -14,7 +14,17 @@ function indexCenter(sizes, indices, offsets, spacing) { return offsets[visibleMiddle] - spacing / 2; } -function resolve(sizes, length, spacing, mode) { +function confine(start, totalSize, bounds) { + if (!bounds) + return start; + const min = bounds.min ?? -Infinity; + const max = bounds.max ?? Infinity; + if (max - min < totalSize) + return (min + max - totalSize) / 2; + return Math.min(Math.max(start, min), max - totalSize); +} + +function resolve(sizes, length, spacing, mode, bounds) { const indices = []; const offsets = []; const positions = sizes.map(() => null); @@ -30,11 +40,17 @@ function resolve(sizes, length, spacing, mode) { } if (indices.length === 0) - return { positions, totalSize: 0 }; + return { + positions, + totalSize: 0 + }; const centerOffset = mode === "geometric" ? totalSize / 2 : indexCenter(sizes, indices, offsets, spacing); - const start = length / 2 - centerOffset; + const start = confine(length / 2 - centerOffset, totalSize, bounds); for (let i = 0; i < indices.length; i++) positions[indices[i]] = start + offsets[i]; - return { positions, totalSize }; + return { + positions, + totalSize + }; } diff --git a/quickshell/Modules/DankBar/CenterSection.qml b/quickshell/Modules/DankBar/CenterSection.qml index deba74be3..cf7d8c930 100644 --- a/quickshell/Modules/DankBar/CenterSection.qml +++ b/quickshell/Modules/DankBar/CenterSection.qml @@ -10,6 +10,9 @@ BarSection { property real totalSize: 0 property real contentStart: 0 property real contentSize: 0 + readonly property var bounds: barContent?.centerBounds ?? null + + onBoundsChanged: layoutTimer.restart() function updateLayout() { positionWidgets(); @@ -54,7 +57,7 @@ BarSection { } applyRoles(entries, participating); - const layout = CenterLayout.resolve(sizes, length, widgetSpacing, SettingsData.centeringMode); + const layout = CenterLayout.resolve(sizes, length, widgetSpacing, SettingsData.centeringMode, bounds); for (let index = 0; index < widgets.length; index++) { const widget = widgets[index]; if (!widget) diff --git a/quickshell/Modules/DankBar/DankBarBody.qml b/quickshell/Modules/DankBar/DankBarBody.qml index dcc0aebd7..4cebd335c 100644 --- a/quickshell/Modules/DankBar/DankBarBody.qml +++ b/quickshell/Modules/DankBar/DankBarBody.qml @@ -259,7 +259,8 @@ Item { readonly property real r: barBackground.rt readonly property bool attachedEdgeCorner: (barBackground.isTop && !atBottom) || (barBackground.isBottom && atBottom) || (barBackground.isLeft && !atRight) || (barBackground.isRight && atRight) readonly property bool wingEdgeCorner: (barBackground.isTop && atBottom) || (barBackground.isBottom && !atBottom) || (barBackground.isLeft && atRight) || (barBackground.isRight && !atRight) - readonly property bool squared: (barBackground.edgeAttached && attachedEdgeCorner) || (barBackground.gothEnabled && wingEdgeCorner) + readonly property bool gothCorner: barBackground.alongWings ? attachedEdgeCorner : wingEdgeCorner + readonly property bool squared: (barBackground.edgeAttached && attachedEdgeCorner) || (barBackground.gothEnabled && gothCorner) x: topBarMouseArea.x + barUnitInset.x + topBarSlide.x + (atRight ? barUnitInset.width - r : 0) y: topBarMouseArea.y + barUnitInset.y + topBarSlide.y + (atBottom ? barUnitInset.height - r : 0) @@ -438,6 +439,10 @@ Item { } readonly property bool edgeAttached: (barConfig?.attachToScreenEdge ?? false) && !(FrameTransitionState.effectiveFrameEnabled && usesFrameBarChrome) + readonly property string barLengthMode: isIsland || (FrameTransitionState.effectiveFrameEnabled && usesFrameBarChrome) ? "full" : (barConfig?.barLengthMode ?? "full") + readonly property bool fitToWidgets: barLengthMode === "fit" + readonly property bool spansEdge: barLengthMode === "full" && ((FrameTransitionState.effectiveFrameEnabled && usesFrameBarChrome) || (barConfig?.barLengthPadding ?? 0) <= 0) + property bool _fitSettled: false property real effectiveSpacing: isIsland || (FrameTransitionState.effectiveFrameEnabled && usesFrameBarChrome) ? 0 : ((edgeAttached || (flattenForMaximizedWindow && hasMaximizedToplevel)) ? 0 : (barConfig?.spacing ?? 4)) Behavior on effectiveSpacing { @@ -451,12 +456,48 @@ Item { readonly property int notificationCount: NotificationService.notifications.length readonly property real effectiveBarThickness: (FrameTransitionState.effectiveFrameEnabled && usesFrameBarChrome) ? SettingsData.frameBarSize : Theme.barThickness(barConfig?.innerPadding ?? 4, _dpr) readonly property real effectiveBarLengthPadding: { + if (barLengthMode === "percent") { + const percent = Math.min(100, Math.max(10, barConfig?.barLengthPercent ?? 80)); + return fittedAvailableLength * (1 - percent / 100) / 2; + } if ((FrameTransitionState.effectiveFrameEnabled && usesFrameBarChrome) || (flattenForMaximizedWindow && hasMaximizedToplevel)) return 0; const pad = Math.max(0, barConfig?.barLengthPadding ?? 0); const length = isVertical ? height : width; return length > 0 ? Math.min(pad, Math.max(0, length / 2 - effectiveSpacing)) : pad; } + readonly property real fittedAvailableLength: { + const length = isVertical ? topBarMouseArea.height : topBarMouseArea.width; + const startGap = isVertical && hasAdjacentTopBar ? 0 : effectiveSpacing; + const endGap = isVertical && hasAdjacentBottomBar ? 0 : effectiveSpacing; + return Math.max(0, length - startGap - endGap); + } + property real fittedLeadingPad: fitToWidgets ? Math.max(0, topBarContent.fittedLeadingPad) : 0 + property real fittedTrailingPad: fitToWidgets ? Math.max(0, topBarContent.fittedTrailingPad) : 0 + readonly property int lengthPaddingStartPx: Theme.px(fitToWidgets ? fittedLeadingPad : effectiveBarLengthPadding, _dpr) + readonly property int lengthPaddingEndPx: Theme.px(fitToWidgets ? fittedTrailingPad : effectiveBarLengthPadding, _dpr) + readonly property bool fitAnimated: fitToWidgets && _fitSettled && (hostWindow?.visible ?? false) && !SettingsData.reduceMotion + + Behavior on fittedLeadingPad { + enabled: barWindow.fitAnimated + NumberAnimation { + duration: Theme.shortDuration + easing.type: Easing.OutCubic + } + } + + Behavior on fittedTrailingPad { + enabled: barWindow.fitAnimated + NumberAnimation { + duration: Theme.shortDuration + easing.type: Easing.OutCubic + } + } + + DeferredAction { + id: fitSettle + onTriggered: barWindow._fitSettled = true + } readonly property bool effectiveOpenOnOverview: (FrameTransitionState.effectiveFrameEnabled && usesFrameBarChrome) ? SettingsData.frameShowOnOverview : (barConfig?.openOnOverview ?? false) readonly property real widgetThickness: Theme.barWidgetThickness(barConfig?.innerPadding ?? 4, _dpr) @@ -468,7 +509,7 @@ Item { readonly property real taskbarStartInset: SettingsData.taskbarInsetForEdge(screen, isVertical ? "top" : "left") readonly property real taskbarEndInset: SettingsData.taskbarInsetForEdge(screen, isVertical ? "bottom" : "right") - readonly property real barSurfaceThickness: Theme.px(effectiveBarThickness + effectiveSpacing + ((renderBarConfig?.gothCornersEnabled ?? false) && !hasMaximizedToplevel ? _wingR : 0), _dpr) + _shadowBuffer + readonly property real barSurfaceThickness: Theme.px(effectiveBarThickness + effectiveSpacing + ((renderBarConfig?.gothCornersEnabled ?? false) && !hasMaximizedToplevel && spansEdge ? _wingR : 0), _dpr) + _shadowBuffer readonly property real hostThickness: isIsland ? (islandHost?.hostThickness ?? islandStripThickness) : barSurfaceThickness readonly property real surfaceImplicitHeight: !isVertical ? hostThickness : 0 readonly property real surfaceImplicitWidth: isVertical ? hostThickness : 0 @@ -477,6 +518,7 @@ Item { updateGpuTempConfig(); _updateHasMaximizedToplevel(); _updateShouldHideForWindows(); + fitSettle.schedule(); } Connections { @@ -546,8 +588,6 @@ Item { id: inputMask readonly property int barThickness: Theme.px(barWindow.isIsland ? barWindow.islandStripThickness : barWindow.effectiveBarThickness + barWindow.effectiveSpacing, barWindow._dpr) - readonly property int lengthPaddingPx: Theme.px(barWindow.effectiveBarLengthPadding, barWindow._dpr) - readonly property bool inOverviewWithShow: CompositorService.overviewActiveOnScreen(barWindow.screenName) && barWindow.effectiveOpenOnOverview readonly property bool effectiveVisible: (barConfig?.visible ?? true) || inOverviewWithShow readonly property bool showing: effectiveVisible && (topBarCore.reveal || inOverviewWithShow) @@ -556,7 +596,7 @@ Item { x: { if (!axis.isVertical) { - return lengthPaddingPx + barWindow.taskbarStartInset; + return barWindow.lengthPaddingStartPx + barWindow.taskbarStartInset; } else { switch (barPos) { case SettingsData.Position.Left: @@ -570,7 +610,7 @@ Item { } y: { if (axis.isVertical) { - return lengthPaddingPx + barWindow.taskbarStartInset; + return barWindow.lengthPaddingStartPx + barWindow.taskbarStartInset; } else { switch (barPos) { case SettingsData.Position.Top: @@ -582,8 +622,8 @@ Item { } } } - width: axis.isVertical ? maskThickness : Math.max(0, parent.width - lengthPaddingPx * 2 - barWindow.taskbarStartInset - barWindow.taskbarEndInset) - height: axis.isVertical ? Math.max(0, parent.height - lengthPaddingPx * 2 - barWindow.taskbarStartInset - barWindow.taskbarEndInset) : maskThickness + width: axis.isVertical ? maskThickness : Math.max(0, parent.width - barWindow.lengthPaddingStartPx - barWindow.lengthPaddingEndPx - barWindow.taskbarStartInset - barWindow.taskbarEndInset) + height: axis.isVertical ? Math.max(0, parent.height - barWindow.lengthPaddingStartPx - barWindow.lengthPaddingEndPx - barWindow.taskbarStartInset - barWindow.taskbarEndInset) : maskThickness } readonly property bool clickThroughEnabled: barConfig?.clickThrough ?? false @@ -847,13 +887,12 @@ Item { Item { id: barUnitInset property int spacingPx: Theme.px(barWindow.effectiveSpacing, barWindow._dpr) - property int lengthPaddingPx: Theme.px(barWindow.effectiveBarLengthPadding, barWindow._dpr) readonly property int islandBandPx: Theme.px(barWindow.islandStripThickness, barWindow._dpr) anchors.fill: parent - anchors.leftMargin: barWindow.isIsland ? (barWindow.isVertical ? (axis.edge === "left" ? 0 : parent.width - islandBandPx) : barWindow.taskbarStartInset) : !barWindow.isVertical ? spacingPx + lengthPaddingPx : (axis.edge === "left" ? spacingPx : 0) - anchors.rightMargin: barWindow.isIsland ? (barWindow.isVertical ? (axis.edge === "right" ? 0 : parent.width - islandBandPx) : barWindow.taskbarEndInset) : !barWindow.isVertical ? spacingPx + lengthPaddingPx : (axis.edge === "right" ? spacingPx : 0) - anchors.topMargin: barWindow.isIsland ? (barWindow.isVertical ? barWindow.taskbarStartInset : (axis.edge === "top" ? 0 : parent.height - islandBandPx)) : barWindow.isVertical ? (barWindow.hasAdjacentTopBar ? 0 : spacingPx) + lengthPaddingPx : (axis.outerVisualEdge() === "bottom" ? 0 : spacingPx) - anchors.bottomMargin: barWindow.isIsland ? (barWindow.isVertical ? barWindow.taskbarEndInset : (axis.edge === "bottom" ? 0 : parent.height - islandBandPx)) : barWindow.isVertical ? (barWindow.hasAdjacentBottomBar ? 0 : spacingPx) + lengthPaddingPx : (axis.outerVisualEdge() === "bottom" ? spacingPx : 0) + anchors.leftMargin: barWindow.isIsland ? (barWindow.isVertical ? (axis.edge === "left" ? 0 : parent.width - islandBandPx) : barWindow.taskbarStartInset) : !barWindow.isVertical ? spacingPx + barWindow.lengthPaddingStartPx : (axis.edge === "left" ? spacingPx : 0) + anchors.rightMargin: barWindow.isIsland ? (barWindow.isVertical ? (axis.edge === "right" ? 0 : parent.width - islandBandPx) : barWindow.taskbarEndInset) : !barWindow.isVertical ? spacingPx + barWindow.lengthPaddingEndPx : (axis.edge === "right" ? spacingPx : 0) + anchors.topMargin: barWindow.isIsland ? (barWindow.isVertical ? barWindow.taskbarStartInset : (axis.edge === "top" ? 0 : parent.height - islandBandPx)) : barWindow.isVertical ? (barWindow.hasAdjacentTopBar ? 0 : spacingPx) + barWindow.lengthPaddingStartPx : (axis.outerVisualEdge() === "bottom" ? 0 : spacingPx) + anchors.bottomMargin: barWindow.isIsland ? (barWindow.isVertical ? barWindow.taskbarEndInset : (axis.edge === "bottom" ? 0 : parent.height - islandBandPx)) : barWindow.isVertical ? (barWindow.hasAdjacentBottomBar ? 0 : spacingPx) + barWindow.lengthPaddingEndPx : (axis.outerVisualEdge() === "bottom" ? spacingPx : 0) onXChanged: barWindow.refreshBlurRegion() onYChanged: barWindow.refreshBlurRegion() onWidthChanged: barWindow.refreshBlurRegion() diff --git a/quickshell/Modules/DankBar/DankBarContent.qml b/quickshell/Modules/DankBar/DankBarContent.qml index bf580f20c..e76e95cb2 100644 --- a/quickshell/Modules/DankBar/DankBarContent.qml +++ b/quickshell/Modules/DankBar/DankBarContent.qml @@ -20,6 +20,39 @@ Item { property var hoverSections: null property real leadingSectionOffset: 0 property real trailingSectionOffset: 0 + readonly property bool fitToWidgets: _hasBarWindow && (barWindow.fitToWidgets ?? false) + readonly property real sectionGap: (_barIsVertical ? vLeftSection : hLeftSection).widgetSpacing + readonly property real leadingImplicitSize: _barIsVertical ? vLeftSection.implicitHeight : hLeftSection.implicitWidth + readonly property real centerImplicitSize: _barIsVertical ? vCenterSection.implicitHeight : hCenterSection.implicitWidth + readonly property real trailingImplicitSize: _barIsVertical ? vRightSection.implicitHeight : hRightSection.implicitWidth + readonly property real fittedStartMargin: _barIsVertical ? _topMargin : _leftMargin + readonly property real fittedEndMargin: _barIsVertical ? _bottomMargin : _rightMargin + readonly property real fittedAvailableSize: fitToWidgets ? Math.max(1, barWindow.fittedAvailableLength - fittedStartMargin - fittedEndMargin) : 0 + readonly property real fittedLeadingReach: leadingImplicitSize > 0 ? leadingImplicitSize + sectionGap : 0 + readonly property real fittedTrailingReach: trailingImplicitSize > 0 ? trailingImplicitSize + sectionGap : 0 + readonly property var fittedCenterSection: _barIsVertical ? vCenterSection : hCenterSection + readonly property real fittedCoreStart: { + if (!fitToWidgets) + return 0; + if (centerImplicitSize > 0) + return fittedCenterSection.contentStart; + const sides = leadingImplicitSize + trailingImplicitSize + (leadingImplicitSize > 0 && trailingImplicitSize > 0 ? sectionGap : 0); + return (barWindow.fittedAvailableLength - sides) / 2 + fittedLeadingReach; + } + readonly property real fittedCoreEnd: !fitToWidgets ? 0 : centerImplicitSize > 0 ? fittedCoreStart + fittedCenterSection.contentSize : fittedCoreStart - (leadingImplicitSize > 0 && trailingImplicitSize > 0 ? sectionGap : 0) + readonly property real fittedLeadingPad: fittedCoreStart - fittedLeadingReach - fittedStartMargin + readonly property var centerBounds: { + if (fitToWidgets) + return { + min: fittedLeadingReach + fittedStartMargin, + max: barWindow.fittedAvailableLength - fittedTrailingReach - fittedEndMargin + }; + return { + min: fittedLeadingReach, + max: (_barIsVertical ? height : width) - fittedTrailingReach + }; + } + readonly property real fittedTrailingPad: barWindow.fittedAvailableLength - fittedCoreEnd - fittedTrailingReach - fittedEndMargin readonly property var workspaceWidget: SettingsData.barWidgetEntry(barConfig, "workspaceSwitcher") function workspaceOption(key) { @@ -228,7 +261,7 @@ Item { } } - readonly property int availableWidth: width + readonly property int availableWidth: fitToWidgets ? fittedAvailableSize : width readonly property int launcherButtonWidth: 40 readonly property int workspaceSwitcherWidth: 120 readonly property int focusedAppMaxWidth: 456 @@ -383,7 +416,7 @@ Item { } axis: barWindow.axis barContent: topBarContent - sectionAvailablePrimarySize: Math.max(1, hCenterSection.x > 0 ? hCenterSection.x : parent.width / 3) + sectionAvailablePrimarySize: topBarContent.fitToWidgets ? topBarContent.fittedAvailableSize : Math.max(1, hCenterSection.x > 0 ? hCenterSection.x : parent.width / 3) } RightSection { @@ -399,7 +432,7 @@ Item { } axis: barWindow.axis barContent: topBarContent - sectionAvailablePrimarySize: Math.max(1, hCenterSection.x > 0 ? parent.width - (hCenterSection.x + hCenterSection.width) : parent.width / 3) + sectionAvailablePrimarySize: topBarContent.fitToWidgets ? topBarContent.fittedAvailableSize : Math.max(1, hCenterSection.x > 0 ? parent.width - (hCenterSection.x + hCenterSection.width) : parent.width / 3) } CenterSection { @@ -407,13 +440,13 @@ Item { objectName: "centerSection" overrideAxisLayout: true forceVerticalLayout: false - anchors { - verticalCenter: parent.verticalCenter - horizontalCenter: parent.horizontalCenter - } + anchors.centerIn: topBarContent.fitToWidgets ? null : parent + anchors.verticalCenter: parent.verticalCenter + x: topBarContent.fitToWidgets ? -(barWindow.lengthPaddingStartPx + topBarContent._leftMargin) : 0 + width: topBarContent.fitToWidgets ? barWindow.fittedAvailableLength : parent.width axis: barWindow.axis barContent: topBarContent - sectionAvailablePrimarySize: Math.max(1, hRightSection.x > 0 ? hRightSection.x - (hLeftSection.x + hLeftSection.width) : parent.width / 3) + sectionAvailablePrimarySize: topBarContent.fitToWidgets ? topBarContent.fittedAvailableSize : Math.max(1, hRightSection.x > 0 ? hRightSection.x - (hLeftSection.x + hLeftSection.width) : parent.width / 3) } } @@ -436,7 +469,7 @@ Item { } axis: barWindow.axis barContent: topBarContent - sectionAvailablePrimarySize: Math.max(1, vCenterSection.y > 0 ? vCenterSection.y : parent.height / 3) + sectionAvailablePrimarySize: topBarContent.fitToWidgets ? topBarContent.fittedAvailableSize : Math.max(1, vCenterSection.y > 0 ? vCenterSection.y : parent.height / 3) } CenterSection { @@ -445,13 +478,13 @@ Item { overrideAxisLayout: true forceVerticalLayout: true width: parent.width - anchors { - verticalCenter: parent.verticalCenter - horizontalCenter: parent.horizontalCenter - } + height: topBarContent.fitToWidgets ? barWindow.fittedAvailableLength : parent.height + anchors.centerIn: topBarContent.fitToWidgets ? null : parent + anchors.horizontalCenter: parent.horizontalCenter + y: topBarContent.fitToWidgets ? -(barWindow.lengthPaddingStartPx + topBarContent._topMargin) : 0 axis: barWindow.axis barContent: topBarContent - sectionAvailablePrimarySize: Math.max(1, vRightSection.y > 0 ? vRightSection.y - (vLeftSection.y + vLeftSection.height) : parent.height / 3) + sectionAvailablePrimarySize: topBarContent.fitToWidgets ? topBarContent.fittedAvailableSize : Math.max(1, vRightSection.y > 0 ? vRightSection.y - (vLeftSection.y + vLeftSection.height) : parent.height / 3) } RightSection { @@ -469,7 +502,7 @@ Item { } axis: barWindow.axis barContent: topBarContent - sectionAvailablePrimarySize: Math.max(1, vCenterSection.y > 0 ? parent.height - (vCenterSection.y + vCenterSection.height) : parent.height / 3) + sectionAvailablePrimarySize: topBarContent.fitToWidgets ? topBarContent.fittedAvailableSize : Math.max(1, vCenterSection.y > 0 ? parent.height - (vCenterSection.y + vCenterSection.height) : parent.height / 3) } } } diff --git a/quickshell/Modules/DankBar/SideSection.qml b/quickshell/Modules/DankBar/SideSection.qml index 82914f825..06f95d7d3 100644 --- a/quickshell/Modules/DankBar/SideSection.qml +++ b/quickshell/Modules/DankBar/SideSection.qml @@ -85,9 +85,11 @@ BarSection { readonly property var widgetItem: widgetLoader.item readonly property bool participates: visible && width > 0 && height > 0 + readonly property bool itemShown: widgetLoader.item?.visible ?? false + visible: widgetLoader.active && widgetLoader.widgetEnabled - width: root.isVertical ? root.width : (widgetLoader.item ? widgetLoader.item.width : 0) - height: widgetLoader.item ? widgetLoader.item.height : 0 + width: !itemShown ? 0 : root.isVertical ? root.width : widgetLoader.item.width + height: itemShown ? widgetLoader.item.height : 0 onXChanged: { if (!root.isVertical) root.refreshBlur(); diff --git a/quickshell/Modules/Plugins/PluginComponent.qml b/quickshell/Modules/Plugins/PluginComponent.qml index 35ea58169..2b67ac870 100644 --- a/quickshell/Modules/Plugins/PluginComponent.qml +++ b/quickshell/Modules/Plugins/PluginComponent.qml @@ -83,6 +83,7 @@ Item { readonly property int iconSize: Theme.barIconSize(barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) readonly property int iconSizeLarge: Theme.barIconSize(barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) + readonly property int textSize: Theme.barTextSize(barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) Component.onCompleted: { loadPluginData(); diff --git a/quickshell/Modules/Settings/DankBarAppearanceTab.qml b/quickshell/Modules/Settings/DankBarAppearanceTab.qml index d652293df..ae0302cfd 100644 --- a/quickshell/Modules/Settings/DankBarAppearanceTab.qml +++ b/quickshell/Modules/Settings/DankBarAppearanceTab.qml @@ -7,6 +7,8 @@ import qs.Modules.Settings.Widgets Item { id: root + readonly property var barLengthModes: ["full", "percent", "fit"] + LayoutMirroring.enabled: I18n.isRtl LayoutMirroring.childrenInherit: true @@ -286,9 +288,43 @@ Item { }) } + SettingsButtonGroupRow { + settingKey: "barLengthMode" + tags: ["bar", "width", "height", "length", "fit", "full", "custom", "percent", "compact", "widgets", "hug", "shrink"] + text: bar.selectedBarIsVertical ? I18n.tr("Height") : I18n.tr("Width") + visible: !bar.selectedBarFrameStyled && !bar.islandOwnsSelectedBarTop + resetStore: bar + resetKeys: ["barLengthMode", "barLengthPercent"] + model: [I18n.tr("Full", "bar length option, the bar spans the whole edge"), I18n.tr("Custom", "bar length option, the bar spans a percentage of the edge"), I18n.tr("Fit", "bar length option, the bar spans only its widgets")] + currentIndex: Math.max(0, root.barLengthModes.indexOf(bar.selectedBarConfig?.barLengthMode ?? "full")) + onSelectionChanged: (index, selected) => { + if (!selected) + return; + SettingsData.updateBarConfig(bar.selectedBarId, { + barLengthMode: root.barLengthModes[index] ?? "full" + }); + } + } + + SettingsSliderRow { + settingKey: "barLengthPercent" + tags: ["bar", "width", "height", "length", "percent", "custom"] + visible: !bar.selectedBarFrameStyled && !bar.islandOwnsSelectedBarTop && (bar.selectedBarConfig?.barLengthMode ?? "full") === "percent" + text: I18n.tr("Percentage") + resetStore: bar + resetKeys: ["barLengthPercent"] + unit: "%" + minimum: 10 + maximum: 100 + value: bar.selectedBarConfig?.barLengthPercent ?? 80 + onSliderDragFinished: finalValue => SettingsData.updateBarConfig(bar.selectedBarId, { + barLengthPercent: finalValue + }) + } + SettingsSliderRow { settingKey: "barLengthPadding" - visible: !bar.selectedBarFrameStyled && !bar.islandOwnsSelectedBarTop + visible: !bar.selectedBarFrameStyled && !bar.islandOwnsSelectedBarTop && (bar.selectedBarConfig?.barLengthMode ?? "full") === "full" text: I18n.tr("Length padding") tags: ["bar", "length", "padding", "size", "shorter", "ends"] resetStore: bar diff --git a/quickshell/PLUGINS/ColorDemoPlugin/ColorDemoWidget.qml b/quickshell/PLUGINS/ColorDemoPlugin/ColorDemoWidget.qml index 6b9084538..82928b213 100644 --- a/quickshell/PLUGINS/ColorDemoPlugin/ColorDemoWidget.qml +++ b/quickshell/PLUGINS/ColorDemoPlugin/ColorDemoWidget.qml @@ -25,7 +25,7 @@ PluginComponent { StyledText { text: root.customColor.toString() - font.pixelSize: Theme.fontSizeSmall + font.pixelSize: root.textSize color: Theme.surfaceText anchors.verticalCenter: parent.verticalCenter } @@ -48,7 +48,7 @@ PluginComponent { StyledText { text: root.customColor.toString() - font.pixelSize: Theme.fontSizeSmall + font.pixelSize: root.textSize color: Theme.surfaceText anchors.horizontalCenter: parent.horizontalCenter } diff --git a/quickshell/PLUGINS/ControlCenterDetailExample/DetailExampleWidget.qml b/quickshell/PLUGINS/ControlCenterDetailExample/DetailExampleWidget.qml index f33898054..eb49e75e7 100644 --- a/quickshell/PLUGINS/ControlCenterDetailExample/DetailExampleWidget.qml +++ b/quickshell/PLUGINS/ControlCenterDetailExample/DetailExampleWidget.qml @@ -118,7 +118,7 @@ PluginComponent { DankIcon { name: root.isEnabled ? "settings" : "settings_off" color: root.isEnabled ? Theme.primary : Theme.surfaceVariantText - font.pixelSize: Theme.iconSize - 4 + size: root.iconSize anchors.verticalCenter: parent.verticalCenter } @@ -128,7 +128,7 @@ PluginComponent { return selected.substring(0, 1); } color: root.isEnabled ? Theme.primary : Theme.surfaceVariantText - font.pixelSize: Theme.fontSizeMedium + font.pixelSize: root.textSize anchors.verticalCenter: parent.verticalCenter } } diff --git a/quickshell/PLUGINS/ControlCenterExample/ControlCenterExampleWidget.qml b/quickshell/PLUGINS/ControlCenterExample/ControlCenterExampleWidget.qml index 450d538c4..ecce2c64b 100644 --- a/quickshell/PLUGINS/ControlCenterExample/ControlCenterExampleWidget.qml +++ b/quickshell/PLUGINS/ControlCenterExample/ControlCenterExampleWidget.qml @@ -1,5 +1,4 @@ import QtQuick -import Quickshell import qs.Common import qs.Services import qs.Widgets @@ -17,13 +16,13 @@ PluginComponent { ccWidgetIsActive: isEnabled onCcWidgetToggled: { - isEnabled = !isEnabled - clickCount += 1 + isEnabled = !isEnabled; + clickCount += 1; if (pluginService) { - pluginService.savePluginData("controlCenterExample", "isEnabled", isEnabled) - pluginService.savePluginData("controlCenterExample", "clickCount", clickCount) + pluginService.savePluginData("controlCenterExample", "isEnabled", isEnabled); + pluginService.savePluginData("controlCenterExample", "clickCount", clickCount); } - ToastService.showInfo("Example Toggle", isEnabled ? "Activated!" : "Deactivated!") + ToastService.showInfo("Example Toggle", isEnabled ? "Activated!" : "Deactivated!"); } horizontalBarPill: Component { @@ -33,14 +32,14 @@ PluginComponent { DankIcon { name: root.isEnabled ? "toggle_on" : "toggle_off" color: root.isEnabled ? Theme.primary : Theme.surfaceVariantText - font.pixelSize: Theme.iconSize - 4 + size: root.iconSize anchors.verticalCenter: parent.verticalCenter } StyledText { text: `${root.clickCount}` color: root.isEnabled ? Theme.primary : Theme.surfaceVariantText - font.pixelSize: Theme.fontSizeMedium + font.pixelSize: root.textSize anchors.verticalCenter: parent.verticalCenter } } @@ -53,14 +52,14 @@ PluginComponent { DankIcon { name: root.isEnabled ? "toggle_on" : "toggle_off" color: root.isEnabled ? Theme.primary : Theme.surfaceVariantText - font.pixelSize: Theme.iconSize - 4 + size: root.iconSize anchors.horizontalCenter: parent.horizontalCenter } StyledText { text: `${root.clickCount}` color: root.isEnabled ? Theme.primary : Theme.surfaceVariantText - font.pixelSize: Theme.fontSizeSmall + font.pixelSize: root.textSize anchors.horizontalCenter: parent.horizontalCenter } } diff --git a/quickshell/PLUGINS/DashCounterExample/CounterWidget.qml b/quickshell/PLUGINS/DashCounterExample/CounterWidget.qml index b9c65e9fe..32f408801 100644 --- a/quickshell/PLUGINS/DashCounterExample/CounterWidget.qml +++ b/quickshell/PLUGINS/DashCounterExample/CounterWidget.qml @@ -8,7 +8,6 @@ PluginComponent { property var popoutService: null readonly property int count: pluginData.count ?? 0 - readonly property real textSize: Theme.barTextSize(barThickness, barConfig?.fontScale, barConfig?.maximizeWidgetText) pillClickAction: (x, y, width, section, screen) => popoutService?.toggleDankDash("plugin_" + pluginId, x, y, width, section, screen) pillRightClickAction: () => pluginService?.savePluginData(pluginId, "count", count + 1) diff --git a/quickshell/PLUGINS/ExampleCompositePlugin/CompositeBarWidget.qml b/quickshell/PLUGINS/ExampleCompositePlugin/CompositeBarWidget.qml index a33b8a509..8bc736b92 100644 --- a/quickshell/PLUGINS/ExampleCompositePlugin/CompositeBarWidget.qml +++ b/quickshell/PLUGINS/ExampleCompositePlugin/CompositeBarWidget.qml @@ -55,7 +55,7 @@ PluginComponent { model: root.displayedEmojis StyledText { text: modelData - font.pixelSize: Theme.fontSizeLarge + font.pixelSize: root.iconSize } } } @@ -70,7 +70,7 @@ PluginComponent { model: root.displayedEmojis StyledText { text: modelData - font.pixelSize: Theme.fontSizeMedium + font.pixelSize: root.iconSize anchors.horizontalCenter: parent.horizontalCenter } } diff --git a/quickshell/PLUGINS/ExampleEmojiPlugin/EmojiWidget.qml b/quickshell/PLUGINS/ExampleEmojiPlugin/EmojiWidget.qml index 5e5a05000..8e5d236ae 100644 --- a/quickshell/PLUGINS/ExampleEmojiPlugin/EmojiWidget.qml +++ b/quickshell/PLUGINS/ExampleEmojiPlugin/EmojiWidget.qml @@ -55,7 +55,7 @@ PluginComponent { model: root.displayedEmojis StyledText { text: modelData - font.pixelSize: Theme.fontSizeLarge + font.pixelSize: root.iconSize } } } @@ -70,7 +70,7 @@ PluginComponent { model: root.displayedEmojis StyledText { text: modelData - font.pixelSize: Theme.fontSizeMedium + font.pixelSize: root.iconSize anchors.horizontalCenter: parent.horizontalCenter } } diff --git a/quickshell/PLUGINS/ExampleStartupCheck/StartupCheckWidget.qml b/quickshell/PLUGINS/ExampleStartupCheck/StartupCheckWidget.qml index 7f9c8b419..f9dbb38f3 100644 --- a/quickshell/PLUGINS/ExampleStartupCheck/StartupCheckWidget.qml +++ b/quickshell/PLUGINS/ExampleStartupCheck/StartupCheckWidget.qml @@ -21,7 +21,7 @@ PluginComponent { StyledText { text: "boregard" - font.pixelSize: Theme.fontSizeSmall + font.pixelSize: root.textSize color: Theme.surfaceText anchors.verticalCenter: parent.verticalCenter } diff --git a/quickshell/PLUGINS/ExampleWithVariants/VariantWidget.qml b/quickshell/PLUGINS/ExampleWithVariants/VariantWidget.qml index c3016e4d5..04e92bdb0 100644 --- a/quickshell/PLUGINS/ExampleWithVariants/VariantWidget.qml +++ b/quickshell/PLUGINS/ExampleWithVariants/VariantWidget.qml @@ -19,14 +19,14 @@ PluginComponent { DankIcon { name: root.displayIcon - size: Theme.iconSize - 8 + size: root.iconSize color: Theme.surfaceText anchors.verticalCenter: parent.verticalCenter } StyledText { text: root.displayText - font.pixelSize: Theme.fontSizeSmall + font.pixelSize: root.textSize font.weight: Theme.fontWeightMedium color: Theme.surfaceText anchors.verticalCenter: parent.verticalCenter @@ -40,14 +40,14 @@ PluginComponent { DankIcon { name: root.displayIcon - size: Theme.iconSize - 8 + size: root.iconSize color: Theme.surfaceText anchors.horizontalCenter: parent.horizontalCenter } StyledText { text: root.displayText - font.pixelSize: Theme.fontSizeSmall + font.pixelSize: root.textSize font.weight: Theme.fontWeightMedium color: Theme.surfaceText anchors.horizontalCenter: parent.horizontalCenter diff --git a/quickshell/PLUGINS/PopoutControlExample/PopoutControlWidget.qml b/quickshell/PLUGINS/PopoutControlExample/PopoutControlWidget.qml index fdbf89f59..7d94578eb 100644 --- a/quickshell/PLUGINS/PopoutControlExample/PopoutControlWidget.qml +++ b/quickshell/PLUGINS/PopoutControlExample/PopoutControlWidget.qml @@ -57,14 +57,14 @@ PluginComponent { DankIcon { name: "widgets" color: Theme.primary - font.pixelSize: Theme.iconSize - 6 + size: root.iconSize anchors.verticalCenter: parent.verticalCenter } StyledText { text: popoutNames[selectedPopout] || "Popouts" color: Theme.primary - font.pixelSize: Theme.fontSizeMedium + font.pixelSize: root.textSize anchors.verticalCenter: parent.verticalCenter } } @@ -77,14 +77,14 @@ PluginComponent { DankIcon { name: "widgets" color: Theme.primary - font.pixelSize: Theme.iconSize - 6 + size: root.iconSize anchors.horizontalCenter: parent.horizontalCenter } StyledText { text: popoutNames[selectedPopout] || "Popouts" color: Theme.primary - font.pixelSize: Theme.fontSizeSmall + font.pixelSize: root.textSize anchors.horizontalCenter: parent.horizontalCenter horizontalAlignment: Text.AlignHCenter } diff --git a/quickshell/tests/bar-content.test.mjs b/quickshell/tests/bar-content.test.mjs index e1109ac1c..0028bfd32 100644 --- a/quickshell/tests/bar-content.test.mjs +++ b/quickshell/tests/bar-content.test.mjs @@ -98,6 +98,19 @@ test("center placement preserves configured anchors, visible fallbacks and geome } }); +test("center placement yields to side sections without overlapping them", () => { + const layout = vm.createContext({}); + vm.runInContext(read("Modules/DankBar/CenterLayout.js").replace(/^\.pragma.*$/m, ""), layout); + const cases = [ + [{ min: 250 }, [250, 284, 368]], + [{ max: 300 }, [122, 156, 240]], + [{ min: 100, max: 200 }, [61, 95, 179]], + [{ min: 0, max: 600 }, [211, 245, 329]] + ]; + for (const [bounds, positions] of cases) + assert.deepEqual(plain(layout.resolve([30, 80, 60], 600, 4, "geometric", bounds)), { positions, totalSize: 178 }); +}); + test("widget lookup preserves component aliases and plugin variant fallback", () => { const builtin = { clockComponent: {}, mediaComponent: {}, mediaActivityComponent: {}, networkComponent: {}, keyboardLayoutNameComponent: {}, appsDockComponent: {} }; const plugins = { example: {}, "example:specific": {} }; diff --git a/quickshell/tests/qml/bar-fit.qml b/quickshell/tests/qml/bar-fit.qml new file mode 100644 index 000000000..1b99af60c --- /dev/null +++ b/quickshell/tests/qml/bar-fit.qml @@ -0,0 +1,321 @@ +import QtQuick +import Quickshell +import qs.Common +import qs.Modules.DankBar +import qs.DankCommon.Common as DC + +ShellRoot { + id: root + + readonly property var steps: [ + { + position: 0, + mode: "fit", + goth: true + }, + { + position: 0, + mode: "fit", + goth: false + }, + { + position: 2, + mode: "fit", + goth: true + }, + { + position: 1, + mode: "fit", + goth: true + }, + { + position: 3, + mode: "fit", + goth: true + }, + { + position: 0, + mode: "fit", + goth: true, + center: false + }, + { + position: 0, + mode: "percent", + goth: true + }, + { + position: 2, + mode: "percent", + goth: true + }, + { + position: 0, + mode: "fit", + goth: true, + wide: true + }, + { + position: 0, + mode: "percent", + goth: false, + wide: true, + percent: 100 + }, + { + position: 0, + mode: "full", + goth: true + } + ] + property int step: 0 + readonly property var current: steps[step] + readonly property bool vertical: current.position === SettingsData.Position.Left || current.position === SettingsData.Position.Right + readonly property int wingRadius: 16 + property var barConfig: ({ + id: "fixture", + enabled: true, + screenPreferences: ["all"], + position: current.position, + spacing: 0, + innerPadding: 4, + barLengthMode: current.mode, + wide: current.wide === true, + barLengthPercent: current.percent ?? 80, + gothCornersEnabled: current.goth, + gothCornerRadiusOverride: true, + gothCornerRadiusValue: wingRadius + }) + property var hyprlandOverviewLoader: null + property bool systemTrayMenuOpen: false + + ScriptModel { + id: widgets + values: [ + { + widgetId: "clock", + id: "clock_0" + } + ] + } + ScriptModel { + id: pairedWidgets + values: [ + { + widgetId: "clock", + id: "clock_1" + }, + { + widgetId: "clock", + id: "clock_2" + } + ] + } + ScriptModel { + id: noWidgets + values: [] + } + ScriptModel { + id: wideWidgets + values: [ + { + widgetId: "clock", + id: "clock_3" + }, + { + widgetId: "clock", + id: "clock_4" + }, + { + widgetId: "clock", + id: "clock_5" + } + ] + } + + FloatingWindow { + id: window + visible: true + implicitWidth: 800 + implicitHeight: 600 + + Item { + width: root.vertical ? body.surfaceImplicitWidth : 800 + height: root.vertical ? 600 : body.surfaceImplicitHeight + + DankBarBody { + id: body + anchors.fill: parent + rootWindow: root + hostWindow: window + modelData: Quickshell.screens[0] + barConfig: root.barConfig + leftWidgetsModel: root.current.wide ? wideWidgets : widgets + centerWidgetsModel: root.current.center === false ? noWidgets : widgets + rightWidgetsModel: pairedWidgets + } + } + } + + function check(condition, message) { + if (!condition) + throw new Error(`step ${step}: ${message}`); + } + + function find(item, predicate, out) { + if (predicate(item)) + out.push(item); + for (const child of item.children || []) + find(child, predicate, out); + return out; + } + + function rectOf(item) { + const point = item.mapToItem(body, 0, 0); + return { + x: point.x, + y: point.y, + width: item.width, + height: item.height, + right: point.x + item.width, + bottom: point.y + item.height, + discX: item.discRect ? point.x + item.discRect.x + item.discRect.width / 2 : 0, + discY: item.discRect ? point.y + item.discRect.y + item.discRect.height / 2 : 0 + }; + } + + function near(a, b) { + return Math.abs(a - b) <= 1.5; + } + + function checkStep() { + const content = body._leftSection.barContent; + const mask = rectOf(body.inputMaskItem); + const surface = rectOf(find(body, item => item.motion !== undefined && item.topLeftRadius !== undefined, [])[0]); + const wings = find(body, item => typeof item.corner === "string" && item.discRect !== undefined && item.visible && item.radius > 0, []).map(rectOf); + const along = item => root.vertical ? item.height : item.width; + const length = root.vertical ? 600 : 800; + const instance = ShellLayout.forScreen(Quickshell.screens[0]).instances.find(instance => instance.barId === "fixture"); + const thickness = body.effectiveBarThickness + body.effectiveSpacing; + const sections = [body._leftSection, body._centerSection, body._rightSection].map(rectOf); + const startEdge = rect => root.vertical ? rect.y : rect.x; + const endEdge = rect => root.vertical ? rect.bottom : rect.right; + + if (current.mode === "full") { + check(near(along(mask), length), `full mask spans the edge, got ${along(mask)}`); + check(near(instance.rowThickness, thickness + root.wingRadius), `full bar row includes the cross wing, got ${instance.rowThickness}`); + check(wings.length === 2 && wings.every(wing => near(wing.y, surface.bottom)), "full bar wings hang below the surface"); + return; + } + + check(near(along(surface), along(mask)) && near(startEdge(surface), startEdge(mask)), "surface matches the mask"); + check(near(root.vertical ? surface.width : surface.height, thickness), `no cross wing thickens the shortened surface, got ${root.vertical ? surface.width : surface.height}`); + check(near(instance.rowThickness, thickness), `shortened row excludes the wing, got ${instance.rowThickness}`); + check([sections[0], sections[2]].every(section => section.x >= surface.x - 1 && section.right <= surface.right + 1 && section.y >= surface.y - 1 && section.bottom <= surface.bottom + 1), "side sections stay inside the surface"); + check(near(startEdge(mask), startEdge(sections[0]) - content.fittedStartMargin) && near(endEdge(mask), endEdge(sections[2]) + content.fittedEndMargin), `mask hugs the outer sections plus the inset, got ${JSON.stringify(mask)}`); + + if (current.mode === "percent") { + const share = (current.percent ?? 80) / 100; + check(near(along(mask), length * share), `percent mask spans ${share} of the edge, got ${along(mask)}`); + check(near(startEdge(mask), length * (1 - share) / 2), `percent mask is centered, got ${startEdge(mask)}`); + const center = body._centerSection; + const coreStart = startEdge(sections[1]) + center.contentStart; + const coreMid = coreStart + center.contentSize / 2; + if (current.wide) { + check(coreMid > length / 2 + 5 && near(coreStart - endEdge(sections[0]), content.sectionGap), `wide left pushes the center right by one widget spacing, got ${coreMid} and ${coreStart - endEdge(sections[0])}`); + check(endEdge(sections[0]) + content.sectionGap <= coreStart + 0.5 && coreStart + center.contentSize + content.sectionGap <= startEdge(sections[2]) + 0.5, "no section overlaps the center"); + } else { + check(near(coreMid, length / 2), `percent keeps the center on the screen center, got ${coreMid}`); + } + } else { + const center = body._centerSection; + const coreStart = startEdge(sections[1]) + center.contentStart; + const coreEnd = coreStart + center.contentSize; + if (current.center === false) { + check(center.contentSize === 0, "empty center has no extent"); + const groupMid = (startEdge(sections[0]) + endEdge(sections[2])) / 2; + check(near(groupMid, length / 2), `side sections center on the edge without center widgets, got ${groupMid}`); + } else if (current.wide) { + check((coreStart + coreEnd) / 2 > length / 2 + 5 && near(startEdge(mask), 0), `wide left pushes the center right and the body hugs the edge, got ${(coreStart + coreEnd) / 2} and ${startEdge(mask)}`); + check(near(coreStart - endEdge(sections[0]), content.sectionGap) && near(startEdge(sections[2]) - coreEnd, content.sectionGap), `sides keep the widget spacing around the pushed center, got ${coreStart - endEdge(sections[0])} and ${startEdge(sections[2]) - coreEnd}`); + } else { + check(center.contentSize > 0 && near((coreStart + coreEnd) / 2, length / 2), `center widgets stay on the screen center, got ${(coreStart + coreEnd) / 2}`); + check(near(coreStart - endEdge(sections[0]), content.sectionGap) && near(startEdge(sections[2]) - coreEnd, content.sectionGap), `side sections hug the center content by the widget spacing, got ${coreStart - endEdge(sections[0])} and ${startEdge(sections[2]) - coreEnd}`); + check(near(content.sectionGap, body._leftSection.widgetSpacing), "section gap equals the widget spacing"); + } + check(current.wide || along(sections[0]) < along(sections[2]), "asymmetric sides in the fixture"); + check(current.wide || along(mask) < length - 100, `fitted mask is shorter than the edge, got ${along(mask)}`); + } + + if (!current.goth) { + check(wings.length === 0, "no wings without goth corners"); + return; + } + check(wings.length === 2, `two along wings, got ${wings.length}`); + const attached = wing => { + switch (current.position) { + case SettingsData.Position.Bottom: + return near(wing.bottom, surface.bottom); + case SettingsData.Position.Left: + return near(wing.x, surface.x); + case SettingsData.Position.Right: + return near(wing.right, surface.right); + default: + return near(wing.y, surface.y); + } + }; + check(wings.every(attached), "wings sit on the attached edge"); + const leading = root.vertical ? near(wings[0].bottom, surface.y) : near(wings[0].right, surface.x); + const trailing = root.vertical ? near(wings[1].y, surface.bottom) : near(wings[1].x, surface.right); + check(leading && trailing, `wings flank the surface ends, got ${JSON.stringify(wings)} around ${JSON.stringify(surface)}`); + const cutAway = wing => { + switch (current.position) { + case SettingsData.Position.Bottom: + return near(wing.discY, wing.y); + case SettingsData.Position.Left: + return near(wing.discX, wing.right); + case SettingsData.Position.Right: + return near(wing.discX, wing.x); + default: + return near(wing.discY, wing.bottom); + } + }; + const cutOutward = root.vertical ? near(wings[0].discY, wings[0].y) && near(wings[1].discY, wings[1].bottom) : near(wings[0].discX, wings[0].x) && near(wings[1].discX, wings[1].right); + check(wings.every(cutAway) && cutOutward, `wing discs are cut away from the edge and the surface, got ${JSON.stringify(wings)}`); + } + + Timer { + id: ticker + interval: 200 + repeat: true + running: SettingsData._hasLoaded + onTriggered: { + try { + if (SettingsData.barConfigs.length !== 1 || SettingsData.barConfigs[0].barLengthMode !== root.current.mode || SettingsData.barConfigs[0].wide !== (root.current.wide === true) || SettingsData.barConfigs[0].barLengthPercent !== (root.current.percent ?? 80) || SettingsData.barConfigs[0].position !== root.current.position || SettingsData.barConfigs[0].gothCornersEnabled !== root.current.goth) { + SettingsData.barConfigs = [root.barConfig]; + return; + } + root.checkStep(); + if (root.step === root.steps.length - 1) { + stop(); + console.log("FIXTURE_PASS"); + Qt.quit(); + return; + } + root.step++; + } catch (error) { + stop(); + console.error("FIXTURE_FAIL", error.message); + Qt.quit(); + } + } + } + + Component.onCompleted: { + Quickshell.watchFiles = false; + DC.Style.theme = Theme; + DC.Style.settings = SettingsData; + DC.I18n.backend = I18n; + SettingsData.reduceMotion = true; + } +} diff --git a/quickshell/tests/run-qml.py b/quickshell/tests/run-qml.py index b05cfeb16..2820def81 100644 --- a/quickshell/tests/run-qml.py +++ b/quickshell/tests/run-qml.py @@ -51,7 +51,7 @@ def main(): parser.error("--jobs must be positive") selected = args.suites or ["widgets", "media", "qt", "logic"] commands = { - "widgets": [sys.executable, "quickshell/tests/run-surface-fixture.py", "--suite", "quickshell/tests/qml/bar-content.qml", "quickshell/tests/qml/frame-bar-flip.qml", "quickshell/tests/qml/instance-routing.qml", "quickshell/tests/qml/font-weights.qml", "quickshell/tests/qml/theme-selected-container.qml", "quickshell/tests/qml/theme-accents.qml", "quickshell/tests/qml/clipboard-preview.qml", "quickshell/tests/qml/launcher-plugin-instances.qml", "quickshell/tests/qml/island-launcher-focus.qml", "quickshell/tests/qml/workspace-switcher.qml", "quickshell/tests/qml/focused-app.qml", "quickshell/tests/qml/slider-wheel.qml", "quickshell/tests/qml/control-center-sizes.qml", "quickshell/tests/qml/dash-navigation.qml", "quickshell/tests/qml/grid-edit-layout.qml", "quickshell/tests/qml/settings-scroll.qml"], + "widgets": [sys.executable, "quickshell/tests/run-surface-fixture.py", "--suite", "quickshell/tests/qml/bar-content.qml", "quickshell/tests/qml/frame-bar-flip.qml", "quickshell/tests/qml/bar-fit.qml", "quickshell/tests/qml/instance-routing.qml", "quickshell/tests/qml/font-weights.qml", "quickshell/tests/qml/theme-selected-container.qml", "quickshell/tests/qml/theme-accents.qml", "quickshell/tests/qml/clipboard-preview.qml", "quickshell/tests/qml/launcher-plugin-instances.qml", "quickshell/tests/qml/island-launcher-focus.qml", "quickshell/tests/qml/workspace-switcher.qml", "quickshell/tests/qml/focused-app.qml", "quickshell/tests/qml/slider-wheel.qml", "quickshell/tests/qml/control-center-sizes.qml", "quickshell/tests/qml/dash-navigation.qml", "quickshell/tests/qml/grid-edit-layout.qml", "quickshell/tests/qml/settings-scroll.qml"], "media": [sys.executable, "quickshell/tests/run-surface-fixture.py", "--suite", "quickshell/tests/qml/media-presentation.qml", "quickshell/tests/qml/media-playback.qml", "quickshell/tests/qml/media-artwork.qml", "quickshell/tests/qml/media-lyrics.qml", "--mpris", "--artwork"], "logic": ["node", "--test", *sorted(str(path.relative_to(repo)) for path in (repo / "quickshell/tests").glob("*.test.mjs"))], } diff --git a/quickshell/translations/settings_search_index.json b/quickshell/translations/settings_search_index.json index 10e73b1c2..4ff5b0d4d 100644 --- a/quickshell/translations/settings_search_index.json +++ b/quickshell/translations/settings_search_index.json @@ -3878,6 +3878,32 @@ ], "parentLabel": "Bar" }, + { + "section": "barLengthMode", + "label": "Height", + "tabIndex": 6, + "category": "Appearance", + "keywords": [ + "appearance", + "bar", + "compact", + "custom", + "fit", + "full", + "height", + "hug", + "length", + "panel", + "percent", + "shrink", + "statusbar", + "taskbar", + "topbar", + "widgets", + "width" + ], + "parentLabel": "Bar" + }, { "section": "islandHighContrast", "label": "High contrast", @@ -4016,6 +4042,27 @@ ], "parentLabel": "Bar" }, + { + "section": "barLengthPercent", + "label": "Percentage", + "tabIndex": 6, + "category": "Appearance", + "keywords": [ + "appearance", + "bar", + "custom", + "height", + "length", + "panel", + "percent", + "percentage", + "statusbar", + "taskbar", + "topbar", + "width" + ], + "parentLabel": "Bar" + }, { "section": "barSize", "label": "Size",