From 17d52d3c88a91f0b3dee73ef2e479b87dfd9b8a1 Mon Sep 17 00:00:00 2001 From: bbedward Date: Tue, 22 Sep 2026 13:44:12 -0400 Subject: [PATCH] media: rebrand to Bento popouts: instantly set empty input region on exit --- .github/workflows/claude-review.yml | 4 +- quickshell/Common/MediaOptions.qml | 11 ++-- quickshell/Modules/DankDash/DashMetrics.qml | 5 +- quickshell/Modules/DankDash/DashRegistry.qml | 2 +- .../{ZurvanChrome.qml => BentoChrome.qml} | 25 ++++---- .../DankDash/Media/MaterialSourceGroup.qml | 4 +- .../Modules/DankDash/Media/MediaPanel.qml | 2 +- .../DankDash/Media/MediaSourceGroup.qml | 64 +++++++++---------- .../Modules/DankDash/MediaPlayerTab.qml | 34 ++++++++-- .../DankDash/Overview/DashOptionsSheet.qml | 1 + .../DankDash/Overview/MediaOverviewCard.qml | 12 ++-- quickshell/Modules/DankDash/WeatherTab.qml | 5 +- quickshell/Services/MprisController.qml | 7 -- quickshell/Widgets/DankPopoutHost.qml | 7 +- quickshell/Widgets/ScrollingText.qml | 20 ++++-- quickshell/shell.qml | 5 +- quickshell/tests/qml/media-lyrics.qml | 10 +-- quickshell/tests/qml/media-presentation.qml | 8 +-- 18 files changed, 133 insertions(+), 93 deletions(-) rename quickshell/Modules/DankDash/Media/{ZurvanChrome.qml => BentoChrome.qml} (93%) diff --git a/.github/workflows/claude-review.yml b/.github/workflows/claude-review.yml index ee2c421eb..00afeffaf 100644 --- a/.github/workflows/claude-review.yml +++ b/.github/workflows/claude-review.yml @@ -88,7 +88,7 @@ jobs: ## Claude review - per inline comment posted this run - Checked: . Model: claude-opus-5. + Checked: . Model: claude-opus-5-5. No findings: "No issues found." plus what was checked. @@ -102,7 +102,7 @@ jobs: Review only. Never edit files, commit, push, approve or request changes. Do not build, vet, format or run tests; CI already does that and the tools are not available here. Post each finding once. Keep comments short. claude_args: | - --model claude-opus-5 + --model claude-opus-5-5 --max-turns 120 --allowedTools "Read,Grep,Glob,LS,Task,mcp__github_inline_comment__create_inline_comment,Bash(gh pr view:*),Bash(gh pr diff:*),Bash(gh pr comment:*),Bash(gh api repos/${{ github.repository }}/pulls/${{ github.event.issue.number }}/comments:*),Bash(git diff:*),Bash(git log:*),Bash(git show:*),Bash(git blame:*)" --disallowedTools "Edit,Write,MultiEdit,NotebookEdit,WebFetch,WebSearch" diff --git a/quickshell/Common/MediaOptions.qml b/quickshell/Common/MediaOptions.qml index 348938166..2f7d08dc1 100644 --- a/quickshell/Common/MediaOptions.qml +++ b/quickshell/Common/MediaOptions.qml @@ -12,14 +12,15 @@ Singleton { albumArtBackdrop: true, albumArtAccent: true, animatedArt: false, - lyrics: true + lyrics: true, + deviceName: true }) - readonly property string defaultPlayerStyle: "zurvan" + readonly property string defaultPlayerStyle: "bento" readonly property var playerStyles: [ { - "value": "zurvan", - "text": "Zurvan" + "value": "bento", + "text": "Bento" }, { "value": "material", @@ -28,7 +29,7 @@ Singleton { ] readonly property var titleFontDefaults: ({ - zurvan: "Notable", + bento: "Notable", material: "ui" }) diff --git a/quickshell/Modules/DankDash/DashMetrics.qml b/quickshell/Modules/DankDash/DashMetrics.qml index d9624a8ee..8e9c18f6d 100644 --- a/quickshell/Modules/DankDash/DashMetrics.qml +++ b/quickshell/Modules/DankDash/DashMetrics.qml @@ -194,8 +194,6 @@ Singleton { readonly property real mediaCardMargin: Theme.spacingL readonly property real mediaArtSize: 150 readonly property real mediaArtSizeDash: 336 - readonly property real mediaArtLowResRatio: 0.5 - readonly property real mediaArtLowResScale: 0.85 readonly property real mediaArtSizeMaterial: 200 readonly property real mediaArtRadius: Theme.cornerRadiusXL readonly property real mediaArtSoftRadiusRatio: 0.3 @@ -204,12 +202,15 @@ Singleton { readonly property real mediaPlayWidthRatio: 1.5 readonly property real mediaPlayWidthStep: 0.75 readonly property real mediaTransportPressScale: 1.2 + readonly property real mediaDeviceNameWidthRatio: 0.42 readonly property real accentSelectedAlpha: 0.2 readonly property real groupIdleAlpha: 0.1 readonly property int mediaPositionPollInterval: 1000 + readonly property int mediaVolumeEchoTimeout: 1500 readonly property int mediaTransitionGraceInterval: 3000 readonly property int mediaPlayerLossGraceInterval: 1500 readonly property real mediaTextScrollSpeed: 40 + readonly property int mediaTextScrollStep: 30 readonly property int mediaLyricsRequestTimeout: 16000 readonly property int mediaLyricsLoadingDelay: 300 readonly property real mediaLyricsPositionTolerance: 0.05 diff --git a/quickshell/Modules/DankDash/DashRegistry.qml b/quickshell/Modules/DankDash/DashRegistry.qml index b7c905c9c..b07829408 100644 --- a/quickshell/Modules/DankDash/DashRegistry.qml +++ b/quickshell/Modules/DankDash/DashRegistry.qml @@ -122,7 +122,7 @@ Singleton { "value": "slanted", "text": I18n.tr("Slanted", "Album artwork shape") } - ]), choice("titleFont", I18n.tr("Title font"), MediaOptions.defaultTitleFont, Theme.fontChoices), toggle("seekbar", I18n.tr("Show seekbar"), MediaOptions.defaults.seekbar), toggle("waveProgress", I18n.tr("Wave progress bars"), MediaOptions.defaults.waveProgress), toggle("albumArtBackdrop", I18n.tr("Album art backdrop"), MediaOptions.defaults.albumArtBackdrop), toggle("albumArtAccent", I18n.tr("Use album art accent"), MediaOptions.defaults.albumArtAccent), toggle("animatedArt", I18n.tr("Apple Music animated covers"), MediaOptions.defaults.animatedArt, I18n.tr("Sends the playing artist and album name to Apple")), toggle("lyrics", I18n.tr("Lyrics", "Media player lyrics button"), MediaOptions.defaults.lyrics, I18n.tr("Sends the playing track, artist and album name to enabled lyrics providers"))] + ]), choice("titleFont", I18n.tr("Title font"), MediaOptions.defaultTitleFont, Theme.fontChoices), toggle("deviceName", I18n.tr("Show device name", "Media player option that puts the audio output device name on its button"), MediaOptions.defaults.deviceName), toggle("seekbar", I18n.tr("Show seekbar"), MediaOptions.defaults.seekbar), toggle("waveProgress", I18n.tr("Wave progress bars"), MediaOptions.defaults.waveProgress), toggle("albumArtBackdrop", I18n.tr("Album art backdrop"), MediaOptions.defaults.albumArtBackdrop), toggle("albumArtAccent", I18n.tr("Use album art accent"), MediaOptions.defaults.albumArtAccent), toggle("animatedArt", I18n.tr("Apple Music animated covers"), MediaOptions.defaults.animatedArt, I18n.tr("Sends the playing artist and album name to Apple")), toggle("lyrics", I18n.tr("Lyrics", "Media player lyrics button"), MediaOptions.defaults.lyrics, I18n.tr("Sends the playing track, artist and album name to enabled lyrics providers"))] }, { "id": "wallpaper", diff --git a/quickshell/Modules/DankDash/Media/ZurvanChrome.qml b/quickshell/Modules/DankDash/Media/BentoChrome.qml similarity index 93% rename from quickshell/Modules/DankDash/Media/ZurvanChrome.qml rename to quickshell/Modules/DankDash/Media/BentoChrome.qml index 06d048e56..bc711ce26 100644 --- a/quickshell/Modules/DankDash/Media/ZurvanChrome.qml +++ b/quickshell/Modules/DankDash/Media/BentoChrome.qml @@ -12,27 +12,25 @@ MediaChromeBase { readonly property bool compact: width < Theme.smallBreakpoint || halfWidth < headerHeight + controls.implicitHeight readonly property real halfWidth: Math.max(0, (width - padding * 2 - Theme.spacingXL) / 2) - readonly property real bodyHeight: Math.max(0, height - padding * 2) // !TODO: expose this variant? playerPaneOpen false gives art beside lyrics, or art alone with hover transport readonly property bool playerPane: root.player.playerPaneOpen readonly property bool controlsColumn: playerPane readonly property bool lyricsColumn: root.player.lyricsOpen && !playerPane readonly property bool splitPanes: controlsColumn || lyricsColumn readonly property bool artTransport: !playerPane && !!root.presentation - readonly property real naturalArtSize: Math.max(0, Math.min(artSize, compact ? width / 2 : halfWidth)) - readonly property real artworkSize: compact ? naturalArtSize : Math.max(naturalArtSize, Math.min(halfWidth, bodyHeight)) + readonly property real naturalArtSize: Math.round(Math.max(0, Math.min(artSize, compact ? width / 2 : halfWidth))) + // Sized from the controls column, never from this item's own height: reading back the height + // the artwork itself sets makes the split settle wherever the current track happened to put it. + readonly property real artworkSize: Math.round(compact ? naturalArtSize : Math.min(halfWidth, Math.max(naturalArtSize, headerHeight + controls.implicitHeight))) readonly property real columnHeight: compact ? artworkSize : Math.max(artworkSize, paneHeight) readonly property real paneHeight: Math.max(artworkSize, headerHeight + controls.implicitHeight) readonly property real paneMinHeight: Math.max(naturalArtSize, headerHeight + controls.implicitHeight) readonly property real headerHeight: sourceRow.height + Theme.spacingS - readonly property real renderScale: Window.window?.devicePixelRatio ?? Screen.devicePixelRatio - readonly property int artSide: TrackArtService.resolvedArtSide - readonly property bool lowResArt: artSide > 0 && artSide < artworkSize * renderScale * DashMetrics.mediaArtLowResRatio artSize: DashMetrics.mediaArtSizeDash surfaceColor: Theme.foregroundColor(Theme.hostSurface) baseHeight: Math.max(DashMetrics.tabMinHeight, padding * 2 + (compact ? naturalArtSize + Theme.spacingXL + paneMinHeight : Math.max(naturalArtSize, paneMinHeight))) - focusTargets: (viewToggle.visible ? [viewToggle] : []).concat(artTransportLoader.item?.focusTargets ?? [], [playerButton], sourceGroup.focusTargets, transport.focusTargets, seekbar.canSeek && seekBlock.visible ? [seekbar] : []) + focusTargets: (viewToggle.visible ? [viewToggle] : []).concat(artTransportLoader.item?.focusTargets ?? [], [playerButton], sourceGroup.focusTargets, transport.focusTargets, seekbar.canSeek && root.hasSeekbar ? [seekbar] : []) panelButtons: [playerButton].concat(sourceGroup.panelButtons) inlineVolume: true @@ -56,10 +54,8 @@ MediaChromeBase { Item { id: art - readonly property real scaledSize: root.artworkSize * (root.lowResArt ? DashMetrics.mediaArtLowResScale : 1) - anchors.centerIn: parent - width: root.compact ? scaledSize : Math.min(root.artworkSize, Math.max(scaledSize, root.headerHeight + controls.implicitHeight)) + width: root.artworkSize height: width HoverHandler { @@ -282,6 +278,7 @@ MediaChromeBase { loop: true fadeEdges: true pxPerMs: DashMetrics.mediaTextScrollSpeed / 1000 + stepInterval: DashMetrics.mediaTextScrollStep } ScrollingText { @@ -294,6 +291,7 @@ MediaChromeBase { loop: true fadeEdges: true pxPerMs: DashMetrics.mediaTextScrollSpeed / 1000 + stepInterval: DashMetrics.mediaTextScrollStep } } } @@ -327,11 +325,13 @@ MediaChromeBase { visible: hasControls } + // Kept in the column for a length-less stream so the pane, and with it the + // artwork it sizes, does not resize when one comes up in the queue. Item { id: seekBlock width: parent.width height: Theme.buttonHeightXS - visible: root.hasSeekbar + visible: root.seekbarEnabled && !!root.presentation LayoutMirroring.enabled: false LayoutMirroring.childrenInherit: true @@ -339,6 +339,7 @@ MediaChromeBase { id: elapsed anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter + visible: root.hasSeekbar text: Format.formatDuration(seekbar.value * (root.presentation?.length ?? 0)) reserveText: duration.text.replace(/\d/g, "8") } @@ -349,6 +350,7 @@ MediaChromeBase { anchors.right: duration.left anchors.margins: Theme.spacingS anchors.verticalCenter: parent.verticalCenter + visible: root.hasSeekbar player: root.player } @@ -356,6 +358,7 @@ MediaChromeBase { id: duration anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter + visible: root.hasSeekbar text: Format.formatDuration(root.presentation?.length ?? 0) reserveText: text.replace(/\d/g, "8") } diff --git a/quickshell/Modules/DankDash/Media/MaterialSourceGroup.qml b/quickshell/Modules/DankDash/Media/MaterialSourceGroup.qml index e8301fae5..39be9a36a 100644 --- a/quickshell/Modules/DankDash/Media/MaterialSourceGroup.qml +++ b/quickshell/Modules/DankDash/Media/MaterialSourceGroup.qml @@ -39,7 +39,7 @@ Row { panelId: "volume" iconName: root.player.getVolumeIcon() enabled: root.player.volumeAvailable - tooltipText: I18n.tr("Volume") + ": " + Math.round(root.player.currentVolume * 100) + "%" + tooltipText: (root.player.usePlayerVolume ? I18n.tr("Media volume") : I18n.tr("Volume")) + ": " + Math.round(root.player.currentVolume * 100) + "%" MediaVolumeWheel { player: root.player @@ -50,7 +50,7 @@ Row { GroupButton { id: devicesButton panelId: "devices" - tooltipText: I18n.tr("Devices", "Media player output device picker") + tooltipText: I18n.tr("Devices", "Media player output device picker") + ": " + AudioService.displayName(AudioService.sink) iconName: AudioService.sinkIcon(AudioService.sink) MediaSinkWheel {} diff --git a/quickshell/Modules/DankDash/Media/MediaPanel.qml b/quickshell/Modules/DankDash/Media/MediaPanel.qml index 0f2fa9690..afcba1799 100644 --- a/quickshell/Modules/DankDash/Media/MediaPanel.qml +++ b/quickshell/Modules/DankDash/Media/MediaPanel.qml @@ -151,7 +151,7 @@ DankBottomSheet { subtitle: [modelData?.trackTitle, modelData?.trackArtist].filter(Boolean).join(" ยท ") selected: modelData === root.player.activePlayer onActivated: { - MprisController.switchActivePlayer(modelData); + MprisController.setActivePlayer(modelData); root.player.panel = ""; } } diff --git a/quickshell/Modules/DankDash/Media/MediaSourceGroup.qml b/quickshell/Modules/DankDash/Media/MediaSourceGroup.qml index e9a332062..1aa63d279 100644 --- a/quickshell/Modules/DankDash/Media/MediaSourceGroup.qml +++ b/quickshell/Modules/DankDash/Media/MediaSourceGroup.qml @@ -4,6 +4,7 @@ import QtQuick import qs.Common import qs.Services import qs.Widgets +import qs.Modules.DankDash import "../../../DankCommon/Common/FocusNavigation.js" as FocusNavigation Item { @@ -11,26 +12,13 @@ Item { required property var player - readonly property var focusTargets: [volumeButton, devicesButton] - readonly property var panelButtons: [volumeButton, devicesButton] + readonly property var focusTargets: [volumeButton, deviceChip, deviceIcon] + readonly property var panelButtons: [volumeButton, deviceChip, deviceIcon] readonly property string devicesLabel: I18n.tr("Devices", "Media player output device picker") readonly property string sinkName: AudioService.displayName(AudioService.sink) - readonly property string fittedSinkName: { - const room = devicesButton.maximumLabelWidth; - // advanceWidth() is not reactive, this read re-evaluates once the font resolves - if (labelMetrics.averageCharacterWidth <= 0) - return ""; - if (Math.ceil(labelMetrics.advanceWidth(sinkName)) <= room) - return sinkName; - const words = sinkName.split(/\s+/); - while (words.length > 1) { - words.pop(); - const shortened = words.join(" "); - if (Math.ceil(labelMetrics.advanceWidth(shortened)) <= room) - return shortened; - } - return ""; - } + readonly property string deviceTooltip: devicesLabel + ": " + sinkName + readonly property bool showSinkName: root.player.options?.deviceName ?? MediaOptions.defaults.deviceName + readonly property string volumeLabel: root.player.usePlayerVolume ? I18n.tr("Media volume") : I18n.tr("Volume") readonly property bool muted: player.usePlayerVolume ? player.currentVolume === 0 : AudioService.sinkSilent readonly property real volumeRatio: player.maxVolumePercent > 0 ? player.currentVolume * 100 / player.maxVolumePercent : 0 @@ -38,13 +26,6 @@ Item { Keys.onPressed: event => event.accepted = FocusNavigation.handleHorizontalKey(event, focusTargets, I18n.isRtl) - FontMetrics { - id: labelMetrics - font.family: Theme.fontFamily - font.pixelSize: Theme.fontSizeMedium - font.weight: Theme.fontWeightMedium - } - DankRingGauge { id: volumeRing @@ -72,7 +53,7 @@ Item { iconName: root.player.getVolumeIcon() iconColor: root.muted ? Theme.onSurfaceVariant : Theme.onSurface enabled: root.player.volumeAvailable - tooltipText: I18n.tr("Volume") + ": " + Math.round(root.player.currentVolume * 100) + "%" + tooltipText: root.volumeLabel + ": " + Math.round(root.player.currentVolume * 100) + "%" onClicked: root.player.togglePanel(panelId) MediaVolumeWheel { @@ -83,22 +64,41 @@ Item { } DankButton { - id: devicesButton + id: deviceChip readonly property string panelId: "devices" anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - maximumWidth: Math.max(0, root.width - volumeRing.width - Theme.spacingXS) - text: root.fittedSinkName || root.devicesLabel + visible: root.showSinkName + maximumWidth: Math.round(root.width * DashMetrics.mediaDeviceNameWidthRatio) + text: root.sinkName iconName: AudioService.sinkIcon(AudioService.sink) - tooltipText: root.fittedSinkName === root.sinkName ? root.devicesLabel : root.sinkName - Accessible.name: root.devicesLabel + ": " + root.sinkName + tooltipText: root.deviceTooltip + Accessible.name: root.deviceTooltip buttonHeight: Theme.buttonHeightXS backgroundColor: MediaAccentService.accentSecondaryContainer textColor: MediaAccentService.onAccentSecondaryContainer horizontalPadding: Theme.spacingM - onClicked: root.player.togglePanel("devices") + onClicked: root.player.togglePanel(panelId) + + MediaSinkWheel {} + } + + DankActionButton { + id: deviceIcon + + readonly property string panelId: "devices" + + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + visible: !root.showSinkName + buttonSize: volumeRing.width + iconName: AudioService.sinkIcon(AudioService.sink) + iconColor: MediaAccentService.onAccentSecondaryContainer + backgroundColor: MediaAccentService.accentSecondaryContainer + tooltipText: root.deviceTooltip + onClicked: root.player.togglePanel(panelId) MediaSinkWheel {} } diff --git a/quickshell/Modules/DankDash/MediaPlayerTab.qml b/quickshell/Modules/DankDash/MediaPlayerTab.qml index e409ef492..41a54fa53 100644 --- a/quickshell/Modules/DankDash/MediaPlayerTab.qml +++ b/quickshell/Modules/DankDash/MediaPlayerTab.qml @@ -45,9 +45,22 @@ Item { } readonly property bool volumeAvailable: !!((activePlayer && activePlayer.volumeSupported && !__isChromeBrowser) || (AudioService.sink && AudioService.sink.audio)) readonly property bool usePlayerVolume: activePlayer && activePlayer.volumeSupported && !__isChromeBrowser - readonly property real currentVolume: usePlayerVolume ? activePlayer.volume : (AudioService.sink?.audio?.volume ?? 0) + readonly property real reportedVolume: usePlayerVolume ? activePlayer.volume : (AudioService.sink?.audio?.volume ?? 0) + readonly property real currentVolume: pendingVolume >= 0 ? pendingVolume : reportedVolume readonly property real maxVolumePercent: usePlayerVolume ? 100 : AudioService.sinkMaxVolume + // MPRIS volume round-trips through the player, so successive steps would each read a stale + // activePlayer.volume and collapse into one. Drive them from the value we last asked for. + property real pendingVolume: -1 + + onReportedVolumeChanged: { + if (pendingVolume >= 0 && Math.abs(reportedVolume - pendingVolume) < 0.005) + clearPendingVolume(); + } + + onUsePlayerVolumeChanged: clearPendingVolume() + onActivePlayerChanged: clearPendingVolume() + implicitWidth: DashMetrics.contentWidthFor(SettingsData.showWeekNumber, DashMetrics.panelColumnsFor(entryId)) implicitHeight: mediaChrome.item?.implicitHeight ?? DashMetrics.tabMinHeight @@ -93,6 +106,12 @@ Item { } } + Timer { + id: pendingVolumeTimer + interval: DashMetrics.mediaVolumeEchoTimeout + onTriggered: root.pendingVolume = -1 + } + MediaPresentation { id: mediaPresentation player: root @@ -155,12 +174,19 @@ Item { return AudioService.sinkVolumeIconName; } + function clearPendingVolume() { + pendingVolume = -1; + pendingVolumeTimer.stop(); + } + function setVolume(ratio) { if (!volumeAvailable) return; const clamped = Math.min(maxVolumePercent / 100, Math.max(0, ratio)); SessionData.suppressOSDTemporarily(); if (usePlayerVolume) { + pendingVolume = clamped; + pendingVolumeTimer.restart(); activePlayer.volume = clamped; return; } @@ -281,12 +307,12 @@ Item { Loader { id: mediaChrome anchors.fill: parent - sourceComponent: root.playerStyle === "material" ? materialChrome : zurvanChrome + sourceComponent: root.playerStyle === "material" ? materialChrome : bentoChrome } Component { - id: zurvanChrome - ZurvanChrome { + id: bentoChrome + BentoChrome { player: root } } diff --git a/quickshell/Modules/DankDash/Overview/DashOptionsSheet.qml b/quickshell/Modules/DankDash/Overview/DashOptionsSheet.qml index 74cfc2328..417d3986c 100644 --- a/quickshell/Modules/DankDash/Overview/DashOptionsSheet.qml +++ b/quickshell/Modules/DankDash/Overview/DashOptionsSheet.qml @@ -47,6 +47,7 @@ CcSheetDialog { iconName: "settings" onClicked: { root.dismiss(); + PopoutService.closeDankDash(); PopoutService.openSettingsWithTab(root.entryId === "media" ? "media_player" : "weather"); } } diff --git a/quickshell/Modules/DankDash/Overview/MediaOverviewCard.qml b/quickshell/Modules/DankDash/Overview/MediaOverviewCard.qml index b5485e16d..79ae074dc 100644 --- a/quickshell/Modules/DankDash/Overview/MediaOverviewCard.qml +++ b/quickshell/Modules/DankDash/Overview/MediaOverviewCard.qml @@ -26,7 +26,7 @@ Card { readonly property string artUrl: TrackArtService.resolvedArtUrl readonly property color accent: MediaAccentService.accent readonly property bool showSeekbar: options.seekbar !== false && !tiny - readonly property bool zurvan: options.playerStyle === "zurvan" + readonly property bool bento: options.playerStyle === "bento" readonly property bool inlineMetadata: compact && titleLabel.implicitHeight + artistLabel.implicitHeight + trackText.spacing > transport.y - Theme.spacingXS readonly property var playbackFocusTargets: [playButton, previousButton, nextButton].concat(seekbar.canSeek ? [seekbar] : []) @@ -167,7 +167,7 @@ Card { id: transport readonly property bool stacked: root.narrow && !root.tiny - readonly property bool centered: root.zurvan && !stacked && !root.tiny + readonly property bool centered: root.bento && !stacked && !root.tiny readonly property bool medium: !root.compact && width >= Theme.buttonHeightM * 4 readonly property real buttonHeight: medium ? Theme.buttonHeightM : Theme.buttonHeightS readonly property real spacing: root.compact || stacked ? Theme.spacingXS : Theme.spacingS @@ -194,7 +194,7 @@ Card { checkable: true checked: root.playing iconFilled: false - radius: Theme.buttonRadius(width, height, buttonSize, pressed, !root.zurvan && checked) + radius: Theme.buttonRadius(width, height, buttonSize, pressed, !root.bento && checked) iconName: root.playing ? "pause" : "play_arrow" Accessible.name: root.playing ? I18n.tr("Pause") : I18n.tr("Play") enabled: !!root.activePlayer?.canTogglePlaying @@ -209,11 +209,11 @@ Card { anchors.bottom: parent.bottom size: transport.medium ? "m" : "s" width: transport.stacked ? (parent.width - transport.spacing) / 2 : (parent.width - playButton.width - transport.spacing * 2) / 2 - variant: root.zurvan ? "tonal" : "standard" - round: !root.zurvan + variant: root.bento ? "tonal" : "standard" + round: !root.bento radius: Theme.buttonRadius(width, height, buttonSize, pressed, round) containerColor: MediaAccentService.accentSecondaryContainer - contentColor: root.zurvan ? MediaAccentService.onAccentSecondaryContainer : Theme.onSecondaryContainer + contentColor: root.bento ? MediaAccentService.onAccentSecondaryContainer : Theme.onSecondaryContainer iconName: "skip_previous" Accessible.name: I18n.tr("Previous") enabled: !!root.activePlayer?.canGoPrevious || (!!root.activePlayer?.canSeek && root.activePlayer.position > 8) diff --git a/quickshell/Modules/DankDash/WeatherTab.qml b/quickshell/Modules/DankDash/WeatherTab.qml index f234a368e..219328e3e 100644 --- a/quickshell/Modules/DankDash/WeatherTab.qml +++ b/quickshell/Modules/DankDash/WeatherTab.qml @@ -279,7 +279,10 @@ Item { DankButton { text: I18n.tr("Settings") iconName: "settings" - onClicked: PopoutService.openSettingsWithTab("weather") + onClicked: { + PopoutService.closeDankDash(); + PopoutService.openSettingsWithTab("weather"); + } } } } diff --git a/quickshell/Services/MprisController.qml b/quickshell/Services/MprisController.qml index 95075d076..336ce28d5 100644 --- a/quickshell/Services/MprisController.qml +++ b/quickshell/Services/MprisController.qml @@ -367,13 +367,6 @@ Singleton { _persistIdentity(activePlayer.identity); } - function switchActivePlayer(player) { - const current = root.activePlayer; - if (current && current !== player && current.canPause) - current.pause(); - setActivePlayer(player); - } - function setActivePlayer(player: MprisPlayer): void { activePlayer = player; if (player) diff --git a/quickshell/Widgets/DankPopoutHost.qml b/quickshell/Widgets/DankPopoutHost.qml index e9fd821b2..b64e38fdf 100644 --- a/quickshell/Widgets/DankPopoutHost.qml +++ b/quickshell/Widgets/DankPopoutHost.qml @@ -1424,8 +1424,6 @@ Item { screen: root.surfaceScreen visible: false color: "transparent" - readonly property bool closeVisualActive: root.shouldBeVisible || root.isClosing - onVisibleChanged: { if (!visible) { root._surfaceFrameReady = false; @@ -1506,13 +1504,14 @@ Item { height: root.maskHeight } + // Empty input region to avoid the animating surface taking pointer events Item { id: contentMaskRect visible: false x: contentContainer.x - contentContainer.horizontalConnectorExtent - root.inputMargin y: contentContainer.y - contentContainer.verticalConnectorExtent - root.inputMargin - width: root.connected || contentWindow.closeVisualActive ? root.renderedAlignedWidth + contentContainer.horizontalConnectorExtent * 2 + root.inputMargin * 2 : 0 - height: root.connected || contentWindow.closeVisualActive ? root.renderedAlignedHeight + contentContainer.verticalConnectorExtent * 2 + root.inputMargin * 2 : 0 + width: root.shouldBeVisible ? root.renderedAlignedWidth + contentContainer.horizontalConnectorExtent * 2 + root.inputMargin * 2 : 0 + height: root.shouldBeVisible ? root.renderedAlignedHeight + contentContainer.verticalConnectorExtent * 2 + root.inputMargin * 2 : 0 } Loader { diff --git a/quickshell/Widgets/ScrollingText.qml b/quickshell/Widgets/ScrollingText.qml index 2c485ae6e..577d5daf0 100644 --- a/quickshell/Widgets/ScrollingText.qml +++ b/quickshell/Widgets/ScrollingText.qml @@ -16,6 +16,7 @@ Item { property real holdStartMs: 2000 property real holdEndMs: 2000 property real pxPerMs: 1 / 60 + property int stepInterval: 60 property real overscroll: 5 property bool animateTextChange: false property bool loop: false @@ -34,6 +35,7 @@ Item { property int scrollDirection: 1 property real scrollHoldMs: holdStartMs property real textShift: 0 + property real lastStepMs: 0 function resetScroll() { scrollOffset = 0; @@ -67,8 +69,11 @@ Item { } onScrollActiveChanged: { - if (!scrollActive) + if (!scrollActive) { resetScroll(); + return; + } + lastStepMs = Date.now(); } clip: true @@ -86,10 +91,13 @@ Item { wrapMode: Text.NoWrap width: root.needsScrolling ? implicitWidth : Math.min(implicitWidth, root.width) elide: SettingsData.scrollTitleEnabled ? Text.ElideNone : Text.ElideRight + // Whole-pixel x plus NativeRendering's glyph snapping quantises the offset twice, which + // turns even motion into a 1px/2px stutter. Both go while the text is actually scrolling. + renderType: root.needsScrolling ? Text.QtRendering : resolvedRenderType x: { if (root.needsScrolling) { const offset = Math.min(root.scrollOffset, root.maxScrollOffset); - return Math.round((root.rightToLeft ? root.width - width + offset : -offset) + root.textShift); + return (root.rightToLeft ? root.width - width + offset : -offset) + root.textShift; } switch (root.horizontalAlignment) { case Text.AlignHCenter: @@ -120,6 +128,7 @@ Item { font: label.font color: label.color wrapMode: Text.NoWrap + renderType: Text.QtRendering Accessible.ignored: true } } @@ -157,13 +166,16 @@ Item { // Timer stepping, not NumberAnimation: a running animation commits frames every vsync (#2863). // When cava frames are already driving renders, scroll steps ride those ticks instead (#2863). Timer { - interval: 60 + interval: root.stepInterval repeat: true running: root.scrollActive onTriggered: { + const now = Date.now(); + const elapsed = Math.min(now - root.lastStepMs, root.stepInterval * 4); + root.lastStepMs = now; if (cavaTickWatch.running) return; - root.stepScroll(60); + root.stepScroll(elapsed); } } diff --git a/quickshell/shell.qml b/quickshell/shell.qml index 77cf8d4fe..821a0d872 100644 --- a/quickshell/shell.qml +++ b/quickshell/shell.qml @@ -34,6 +34,7 @@ ShellRoot { DC.Log.backend = Log; DC.Host.session = SessionService; DC.Host.cache = CacheData; + void IconThemeService.ready; if (entrypoint.runGreeter) return; // Build the polkit agent here, outside incubation: first-touching it from a Connections target during DMSShell's async load crashed QQmlConnections::connectSignalsToMethods. @@ -58,7 +59,7 @@ ShellRoot { Loader { id: shellCoreLoader - active: !entrypoint.runGreeter && IconThemeService.ready + active: !entrypoint.runGreeter asynchronous: true source: "ShellCore.qml" onLoaded: dmsShellLoader.setSource("DMSShell.qml", { @@ -73,7 +74,7 @@ ShellRoot { Loader { id: dmsGreeterLoader - active: entrypoint.runGreeter && IconThemeService.ready + active: entrypoint.runGreeter asynchronous: false source: "DMSGreeter.qml" } diff --git a/quickshell/tests/qml/media-lyrics.qml b/quickshell/tests/qml/media-lyrics.qml index 4efdd9c42..7efe8d303 100644 --- a/quickshell/tests/qml/media-lyrics.qml +++ b/quickshell/tests/qml/media-lyrics.qml @@ -202,7 +202,7 @@ ShellRoot { SettingsData.reduceMotion = true; SettingsData.dashOptions = { media: { - playerStyle: "zurvan", + playerStyle: "bento", lyrics: true } }; @@ -237,7 +237,7 @@ ShellRoot { check(media.lyrics.lines.length === 5 && media.lyrics.lines[2].x.includes("backing vocal"), "same-time lyrics stay together"); let overlay = media.lyricsFocusTarget; check(overlay && overlay.activeFocus, "lyrics receives keyboard focus"); - check(overlay.width === artwork.width && overlay.height === artwork.height, "Zurvan lyrics stay within the artwork shape"); + check(overlay.width === artwork.width && overlay.height === artwork.height, "Bento lyrics stay within the artwork shape"); const openCorners = artCorners(artwork); check(openCorners === corners, "opening lyrics leaves pixels outside the artwork untouched: " + corners + " -> " + openCorners); const line = find(overlay, item => item.text === root.longLine && item.truncated !== undefined); @@ -248,7 +248,7 @@ ShellRoot { check(line.lineCount === wrappedLineCount, "lyric highlighting preserves line breaks"); source.position = 10; input.wait(30); - capture("zurvan"); + capture("bento"); input.keyClick(Qt.Key_PageDown); input.wait(30); check(!overlay.following && input.findChild(overlay, "followPlayback")?.visible, "browsing pauses automatic following"); @@ -413,7 +413,7 @@ ShellRoot { check(media.lyrics.state === "ready" && !media.lyrics.synced, "embedded lyrics remain available with all providers off"); SettingsData.dashOptions = { media: { - playerStyle: "zurvan", + playerStyle: "bento", lyrics: true, artStyle: "circle" } @@ -438,7 +438,7 @@ ShellRoot { SettingsData.reduceMotion = false; SettingsData.dashOptions = { media: { - playerStyle: "zurvan", + playerStyle: "bento", lyrics: true } }; diff --git a/quickshell/tests/qml/media-presentation.qml b/quickshell/tests/qml/media-presentation.qml index 5bc139c32..124bcc41c 100644 --- a/quickshell/tests/qml/media-presentation.qml +++ b/quickshell/tests/qml/media-presentation.qml @@ -199,11 +199,11 @@ ShellRoot { viewport.width = 780; SettingsData.dashOptions = { media: { - playerStyle: "zurvan" + playerStyle: "bento" } }; input.wait(30); - check(!!find(media, item => item.loop === true), "switching back restores Zurvan"); + check(!!find(media, item => item.loop === true), "switching back restores Bento"); media.lyricsOpen = false; media.playerPaneOpen = false; input.wait(30); @@ -215,7 +215,7 @@ ShellRoot { input.wait(30); SettingsData.dashOptions = { media: { - playerStyle: "zurvan", + playerStyle: "bento", lyrics: false } }; @@ -225,7 +225,7 @@ ShellRoot { check(!!lyricsToggle && !lyricsToggle.visible, "lyrics option gates the button"); SettingsData.dashOptions = { media: { - playerStyle: "zurvan", + playerStyle: "bento", lyrics: true } };