Compare commits

...

10 commits

Author SHA1 Message Date
5d83caaaa1 Add Gaming Shell screenshot to README
Include the new gaming shell preview image in the screenshot section and add its EUPL-1.2 SPDX sidecar.
2026-06-01 16:09:47 +02:00
75a0f7a21e Rework Game Center into console-style capsule rails
Restructure the overlay around landscape 16:9 media capsules, clearer focus borders, tighter search/filter rails, and stronger handheld vs big-screen spacing hierarchy.
2026-06-01 15:49:44 +02:00
434f46403c Add compact-mode sizing hooks for gaming rails
Expose compactMode in running games and quick settings so handheld layouts can tighten heading scale, tile size, and panel width without touching behavior.
2026-06-01 15:49:39 +02:00
00643ab5f5 Tighten desktop experience paragraph in README 2026-06-01 15:21:18 +02:00
43fe97dc11 Drop MobileShell motion dependency in KWin QML
Replace MobileShell motion imports/animations with local Qt Quick ColorAnimation and NumberAnimation timings so decoration and snap-assist effects animate consistently without shell plugin coupling.
2026-06-01 15:20:53 +02:00
33ebcce63b Polish gaming mode session flow and legends
Save and restore dynamic tiling across gaming sessions, close conflicting homescreen surfaces when entering gaming mode, and make gamepad legends adaptive for non-gamepad input with wrapped labels.
2026-06-01 15:20:36 +02:00
b0ce6acdea Keep inactive quick-setting tiles clickable
Restore the delegate/status-row enabled shadow property so visual state no longer binds to Item.enabled.

Without this, off tiles become input-disabled and cannot be turned back on.
2026-06-01 15:16:46 +02:00
0ac87a712e Expand dynamic theming regression coverage
Assert the shared surface helper, accent-aware panels, Folio chrome,
and dock labels use the new color path.
2026-06-01 12:38:22 +02:00
b1f8d17f88 Apply accent-aware shell surface theming
Add a shared SurfaceColors helper and route shell, action drawer, popup,
and Folio surfaces through accent-aware background colors.
2026-06-01 12:38:17 +02:00
60a0246256 Add dynamic tiling screenshot to README
Include the new SHIFT dynamic tiling screenshot in README and add its SPDX license sidecar.
2026-06-01 09:54:41 +02:00
25 changed files with 390 additions and 210 deletions

View file

@ -1,22 +1,47 @@
# Shift
SHIFT is an independent shell fork focused on convergence. It builds on KDE Plasma Mobile, keeps the upstream phone UI available, and layers desktop-oriented behaviour on top of the same session.
SHIFT is a convergent environment for desktops, gaming consoles, tablets, and smartphones.
SHIFT tracks [plasma-mobile](https://invent.kde.org/plasma/plasma-mobile) as its upstream base.
The upstream phone UI remains intact; convergence-specific behaviour is added on top.
What does convergence mean? This project aims to offer the same environment across different form factors, with an interface that adapts to the device, input type, and required user experience.
For example: Smartphones connected to an external display can benefit from a graphical experience change from undocked to docked, transforming the workspace from mobile to desktop. Another example: 2-in-1 laptops that convert to tablets or have 360° rotatable displays can switch the graphical interface while offering a tablet or desktop experience.
Desktop:
SHIFT aims to offer a complete desktop experience, featuring a dockbar, a top bar, an app drawer, and an actions drawer with a notification history, a System Tray, and a Quick Settings Menu. The desktop experience is available in dynamic tiling mode or with classic floating windows.
Smartphone / Tablet:
The environment is capable of adapting its interface to work with tablets and smartphones, adopting a touch-first interface and workflow optimized for touchscreens.
Gaming Shell:
The Gaming Shell aims to be optimized for gamepad navigation, offering integration with game launchers and game centers. The gaming experience must also adapt to different factors, adapting to handheld devices and gaming stations that also serve as workstations.
### Screenshot
![SHIFT main interface](screenshots/SHIFT-main.png)
![SHIFT dynamic tiling interface](screenshots/SHIFT-dynamic_tiling.png)
![SHIFT gaming shell interface](screenshots/SHIFT-Gaming_Shell.png)
### What convergence mode changes
* Navigation panel replaced by a dock with running-app indicators,
favourites, context menus, and hover tooltips
* App drawer opens as a floating popup above the dock
* Window management: edge tiling, edge maximize, close buttons, task
context menus, Overview integration
* Status bar gains a system tray, date display, and hover highlights
* Screen space reserved for the dock via layer-shell exclusive zone
* Desktop niceties: right-click wallpaper settings, minimize-all on
home press, clickable page indicators
* The old navigation panel is replaced by a unified dock that combines
pinned apps, running windows, desktop pager controls, and quick
actions such as Home and Overview.
* The dock supports desktop workflows: pin and unpin, drag reorder,
grouped window entries, middle-click close, and thumbnail previews.
* The app drawer and action drawer run as convergence surfaces (bounded
and shaped), with calendar, notifications, quick settings, and
desktop-focused utility actions.
* The status bar integrates tray/date behavior for desktop usage and is
coordinated with the convergence drawers and workspace chrome.
* Screen space is explicitly reserved for the dock, with geometry and
input-region guards to keep overlays and windows stable.
* Window workflows are desktop-oriented: edge tiling/maximize,
dynamic-tiling actions from task menus, and optional snap-layout
behavior.
* Overview and virtual desktop operations are integrated into dock and
drawer actions, including desktop/window management entries.
* Theme behavior is convergence-aware, with Shift defaults and
wallpaper-driven dynamic theming applied to shell surfaces.
### Locations
@ -39,7 +64,9 @@ cmake --install build
Compatibility-sensitive identifiers such as `org.kde.plasma.mobileshell`
are intentionally preserved in the runtime and build instructions. They are
part of the current compatibility boundary, not the public product name.
part of the current compatibility boundary.
SHIFT tracks [plasma-mobile](https://invent.kde.org/plasma/plasma-mobile) as its upstream base.
The upstream phone UI remains intact; convergence-specific behaviour is added on top.
### Disclaimer
@ -47,7 +74,7 @@ SHIFT is an independent project based on KDE Plasma Mobile.
It is **not affiliated with or endorsed by** KDE or the KDE community.
Some visual elements (such as icons or graphical assets) may originate from KDE Plasma Mobile and are used in accordance with their respective licenses. These elements may be replaced in future versions as the project evolves.
Some visual elements may originate from KDE Plasma Mobile and are used in accordance with their respective licenses. These elements may be replaced in future versions as the project evolves.
All trademarks, including KDE, belong to their respective owners.

View file

@ -26,6 +26,7 @@ set_source_files_properties(
qml/components/AppLaunch.qml
qml/components/Constants.qml
qml/components/Motion.qml
qml/components/SurfaceColors.qml
qml/dataproviders/AudioInfo.qml
qml/dataproviders/BatteryInfo.qml
qml/dataproviders/BluetoothInfo.qml
@ -60,6 +61,7 @@ ecm_target_qml_sources(mobileshellplugin SOURCES
qml/components/ScreenEdgeDragEffect.qml
qml/components/StartupFeedbackPanelFill.qml
qml/components/StartupFeedbackWindows.qml
qml/components/SurfaceColors.qml
qml/components/TextDropShadow.qml
qml/components/VelocityCalculator.qml

View file

@ -77,10 +77,7 @@ Item {
// Background color
Rectangle {
anchors.fill: parent
color: Qt.rgba(Kirigami.Theme.backgroundColor.r,
Kirigami.Theme.backgroundColor.g,
Kirigami.Theme.backgroundColor.b,
0.9)
color: MobileShell.SurfaceColors.withAlpha(MobileShell.SurfaceColors.accentSurface(Kirigami.Theme.backgroundColor, 0.18, 0.10), 0.9)
Behavior on color { MobileShell.MotionColorAnimation { type: MobileShell.Motion.StandardDecel } }
opacity: {
let base = Math.max(0, Math.min(brightnessPressedValue, actionDrawer.offset / root.minimizedQuickSettingsOffset));

View file

@ -28,6 +28,7 @@ QQC2.Popup {
padding: Kirigami.Units.smallSpacing
readonly property int popupAnimationDuration: MobileShell.Motion.duration(MobileShell.Motion.EffectsFast)
readonly property color surfaceColor: MobileShell.SurfaceColors.accentSurface(Kirigami.Theme.backgroundColor, 0.24, 0.12)
property string currentPluginId: ""
property Item __currentItem: null
@ -81,11 +82,11 @@ QQC2.Popup {
}
background: Kirigami.ShadowedRectangle {
color: Kirigami.Theme.backgroundColor
color: popup.surfaceColor
radius: Kirigami.Units.cornerRadius
border.color: Kirigami.ColorUtils.linearInterpolation(
Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, 0.2)
popup.surfaceColor, Kirigami.Theme.textColor, 0.2)
border.width: 1
shadow.size: Kirigami.Units.gridUnit

View file

@ -74,7 +74,7 @@ Item {
id: actionDrawerSurfacePath
readonly property real cornerRadius: actionDrawerSurface.cornerRadius
fillColor: Kirigami.Theme.backgroundColor
fillColor: MobileShell.SurfaceColors.accentSurface(Kirigami.Theme.backgroundColor, 0.32, 0.18)
strokeWidth: 0
startX: actionDrawerSurface.width
startY: 0

View file

@ -26,6 +26,11 @@ MobileShell.BaseItem {
required property string text
required property string status
required property string icon
// Visual active-state of the tile. Declared here to shadow the built-in
// Item.enabled: binding the real Item.enabled would disable the contentItem
// MouseArea, making an inactive tile impossible to tap (and thus turn on).
// Tiles are removed from the list via `available`, never disabled.
required property bool enabled
required property string settingsCommand
required property var toggleFunction

View file

@ -18,6 +18,10 @@ Item {
required property string text
required property string status
required property string icon
// Visual active-state only. Shadows the built-in Item.enabled so binding it
// does not disable the inner toggle/detail MouseAreas (which would make an
// inactive service impossible to switch on).
required property bool enabled
required property var toggleFunction
property bool compact: false

View file

@ -28,6 +28,7 @@ QQC2.Popup {
readonly property int popupAnimationDuration: MobileShell.Motion.duration(MobileShell.Motion.EffectsFast)
readonly property int trayItemCount: trayList.count
readonly property color surfaceColor: MobileShell.SurfaceColors.accentSurface(Kirigami.Theme.backgroundColor, 0.24, 0.12)
function show() {
popup.open();
@ -38,11 +39,11 @@ QQC2.Popup {
}
background: Kirigami.ShadowedRectangle {
color: Kirigami.Theme.backgroundColor
color: popup.surfaceColor
radius: Kirigami.Units.cornerRadius
border.color: Kirigami.ColorUtils.linearInterpolation(
Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, 0.2)
popup.surfaceColor, Kirigami.Theme.textColor, 0.2)
border.width: 1
shadow.size: Kirigami.Units.gridUnit

View file

@ -13,7 +13,7 @@ Item {
property bool active: false
property bool stateLayerEnabled: true
property color color: Kirigami.Theme.textColor
property color activeColor: Kirigami.Theme.highlightColor
property color activeColor: SurfaceColors.accent()
property real hoverOpacity: 0.08
property real pressedOpacity: 0.14
property real activeOpacity: 0.12

View file

@ -48,15 +48,24 @@ Item {
// adjust color depending on panel type
property color panelColor: {
let tintPercent
let accentTintDark
let accentTintLight
if (panelType === PanelBackground.PanelType.Popup) {
tintPercent = 0.035
accentTintDark = 0.16
accentTintLight = 0.08
} else if (panelType === PanelBackground.PanelType.Base || panelType === PanelBackground.PanelType.Stacked || panelType === PanelBackground.PanelType.Flat) {
tintPercent = 0
accentTintDark = 0.18
accentTintLight = 0.10
} else {
tintPercent = 0.06
accentTintDark = 0.22
accentTintLight = 0.12
}
return Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.backgroundColor, "white", tintPercent)
const baseColor = Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.backgroundColor, "white", tintPercent)
return SurfaceColors.accentSurface(baseColor, accentTintDark, accentTintLight)
}
// in some circumstances, panels can change there type
// for example, popup notifition when opening the popup notifition drawer

View file

@ -0,0 +1,47 @@
// SPDX-FileCopyrightText: 2026 Marco Allegretti
// SPDX-License-Identifier: EUPL-1.2
pragma Singleton
import QtQuick
import org.kde.kirigami as Kirigami
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
QtObject {
function hasColor(color) {
return color && color.a > 0
}
function accent() {
if (hasColor(ShellSettings.Settings.accentColor)) {
return ShellSettings.Settings.accentColor
}
if ((ShellSettings.Settings.wallpaperThemeEnabled || ShellSettings.Settings.wallpaperAccentEnabled)
&& hasColor(ShellSettings.Settings.wallpaperThemeColor)) {
return ShellSettings.Settings.wallpaperThemeColor
}
return Kirigami.Theme.highlightColor
}
function mix(base, overlay, ratio) {
return Qt.rgba(
base.r + (overlay.r - base.r) * ratio,
base.g + (overlay.g - base.g) * ratio,
base.b + (overlay.b - base.b) * ratio,
base.a + (overlay.a - base.a) * ratio)
}
function withAlpha(color, alpha) {
return Qt.rgba(color.r, color.g, color.b, alpha)
}
function accentSurface(base, darkRatio, lightRatio) {
if (!ShellSettings.Settings.wallpaperThemeEnabled && !ShellSettings.Settings.wallpaperAccentEnabled) {
return base
}
const darkSurface = Kirigami.ColorUtils.brightnessForColor(base) === Kirigami.ColorUtils.Dark
return mix(base, accent(), darkSurface ? darkRatio : lightRatio)
}
}

View file

@ -57,7 +57,7 @@ Window {
Kirigami.Theme.colorSet: Kirigami.Theme.Complementary
Kirigami.Theme.inherit: false
readonly property color backgroundColor: Qt.darker(Qt.rgba(Kirigami.Theme.backgroundColor.r, Kirigami.Theme.backgroundColor.g, Kirigami.Theme.backgroundColor.b, 0.95), 1.05)
readonly property color backgroundColor: Qt.darker(MobileShell.SurfaceColors.withAlpha(MobileShell.SurfaceColors.accentSurface(Kirigami.Theme.backgroundColor, 0.18, 0.10), 0.95), 1.05)
color: popupDrawerOpened && visible ? backgroundColor : "transparent"
Behavior on color {
MobileShell.MotionColorAnimation {

View file

@ -34,7 +34,7 @@ Window {
LayerShell.Window.layer: LayerShell.Window.LayerOverlay
LayerShell.Window.exclusionZone: -1
readonly property color backgroundColor: Qt.darker(Qt.rgba(Kirigami.Theme.backgroundColor.r, Kirigami.Theme.backgroundColor.g, Kirigami.Theme.backgroundColor.b, 0.95), 1.05)
readonly property color backgroundColor: Qt.darker(MobileShell.SurfaceColors.withAlpha(MobileShell.SurfaceColors.accentSurface(Kirigami.Theme.backgroundColor, 0.18, 0.10), 0.95), 1.05)
readonly property int overlayAnimationDuration: Math.round(MobileShell.Motion.duration(MobileShell.Motion.SpatialSlow) * 1.25)
Kirigami.Theme.colorSet: Kirigami.Theme.Complementary

View file

@ -57,7 +57,8 @@ MouseArea {
return Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.18)
}
if (active) {
return Qt.rgba(Kirigami.Theme.highlightColor.r, Kirigami.Theme.highlightColor.g, Kirigami.Theme.highlightColor.b, hovered ? 0.18 : 0.12)
const accent = MobileShell.SurfaceColors.accent()
return Qt.rgba(accent.r, accent.g, accent.b, hovered ? 0.18 : 0.12)
}
if (hovered) {
return Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.08)
@ -756,7 +757,7 @@ MouseArea {
PC3.Label {
anchors.centerIn: parent
text: (leftDesktopBtn.index + 1).toString()
color: leftDesktopBtn.isCurrent ? Kirigami.Theme.highlightColor : Kirigami.Theme.textColor
color: leftDesktopBtn.isCurrent ? MobileShell.SurfaceColors.accent() : Kirigami.Theme.textColor
font.pixelSize: Math.round(parent.height * 0.3)
font.bold: leftDesktopBtn.isCurrent
}
@ -1487,7 +1488,7 @@ MouseArea {
Rectangle {
anchors.fill: parent
color: Kirigami.Theme.backgroundColor
color: MobileShell.SurfaceColors.accentSurface(Kirigami.Theme.backgroundColor, 0.24, 0.12)
border.color: Qt.rgba(
Kirigami.Theme.textColor.r,
Kirigami.Theme.textColor.g,
@ -1544,9 +1545,7 @@ MouseArea {
anchors.fill: parent
radius: Kirigami.Units.cornerRadius
color: thumbEntry.containsMouse
? Qt.rgba(Kirigami.Theme.highlightColor.r,
Kirigami.Theme.highlightColor.g,
Kirigami.Theme.highlightColor.b, 0.15)
? MobileShell.SurfaceColors.withAlpha(MobileShell.SurfaceColors.accent(), 0.15)
: "transparent"
}
@ -1790,7 +1789,7 @@ MouseArea {
width: Kirigami.Units.smallSpacing * 3
height: Math.max(2, Math.round(Kirigami.Units.devicePixelRatio))
radius: height / 2
color: Kirigami.Theme.highlightColor
color: MobileShell.SurfaceColors.accent()
opacity: taskDelegate.dynamicTilingMaximized ? 0.95 : 0
Behavior on opacity {
@ -1861,7 +1860,7 @@ MouseArea {
width: taskDelegate.model.IsActive === true ? Kirigami.Units.smallSpacing * 3 : Kirigami.Units.smallSpacing * 1.5
height: Math.max(2, Math.round(Kirigami.Units.devicePixelRatio))
radius: height / 2
color: Kirigami.Theme.highlightColor
color: MobileShell.SurfaceColors.accent()
opacity: taskDelegate.model.IsActive === true ? 1.0 : 0.45
Behavior on width {

View file

@ -33,6 +33,7 @@ Window {
readonly property string backButtonLabel: GamingShell.GamepadManager.buttonLabel(GamingShell.GamepadManager.ButtonB)
readonly property string closeButtonLabel: GamingShell.GamepadManager.buttonLabel(GamingShell.GamepadManager.ButtonX)
readonly property string exitButtonLabel: GamingShell.GamepadManager.buttonLabel(GamingShell.GamepadManager.ButtonY)
readonly property string leftShoulderLabel: GamingShell.GamepadManager.buttonLabel(GamingShell.GamepadManager.ButtonLeftShoulder)
readonly property string rightShoulderLabel: GamingShell.GamepadManager.buttonLabel(GamingShell.GamepadManager.ButtonRightShoulder)
readonly property string quickSettingsButtonLabel: GamingShell.GamepadManager.buttonLabel(GamingShell.GamepadManager.ButtonBack)
@ -40,6 +41,41 @@ Window {
readonly property int shortAnimationDuration: MobileShell.Motion.duration(MobileShell.Motion.EffectsFast)
readonly property int longAnimationDuration: MobileShell.Motion.duration(MobileShell.Motion.EffectsDefault)
readonly property int launchFadeDuration: MobileShell.Motion.duration(MobileShell.Motion.StandardAccel)
readonly property int shortestSide: Math.min(width, height)
readonly property bool compactMode: !ShellSettings.Settings.convergenceModeEnabled && shortestSide <= Kirigami.Units.gridUnit * 50
readonly property bool bigScreenMode: !compactMode
readonly property int horizontalPadding: compactMode ? Kirigami.Units.largeSpacing : Kirigami.Units.largeSpacing * 2
readonly property int verticalPadding: compactMode ? Kirigami.Units.largeSpacing : Kirigami.Units.largeSpacing * 2
readonly property real gridMinCellSize: compactMode ? Kirigami.Units.gridUnit * 6.8 : Kirigami.Units.gridUnit * 8.8
// Steam library assets heavily favor wide capsules and 16:9 media surfaces.
// Keep game tiles landscape-first to avoid mobile-style portrait cards.
readonly property real capsuleArtAspect: 16 / 9
function controlLegendText() {
if (GamingShell.GamepadManager.hasGamepad) {
if (runningGames.activeFocus) {
return i18n("%1: Select %2: Close %3: Back %4: Exit %5: Settings %6: Search",
actionButtonLabel, closeButtonLabel, backButtonLabel, exitButtonLabel,
quickSettingsButtonLabel, searchButtonLabel)
}
if (recentList.activeFocus) {
return i18n("%1: Play %2: Details %3: Back %4: Exit %5: Settings %6: Search",
actionButtonLabel, closeButtonLabel, backButtonLabel, exitButtonLabel,
quickSettingsButtonLabel, searchButtonLabel)
}
return i18n("%1: Play %2: Details %3: Back %4: Exit %5/%6: Filter %7: Settings %8: Search",
actionButtonLabel, closeButtonLabel, backButtonLabel, exitButtonLabel,
leftShoulderLabel, rightShoulderLabel, quickSettingsButtonLabel, searchButtonLabel)
}
if (runningGames.activeFocus) {
return i18n("Mouse: click a running game to focus it. Keyboard: arrows move between cards, Enter selects, Esc closes.")
}
if (recentList.activeFocus) {
return i18n("Mouse: click a recent game to play it. Keyboard: arrows move between cards, Enter plays, Esc closes.")
}
return i18n("Mouse: click a game or details button. Keyboard: arrows navigate, Enter plays, I shows details, Esc closes.")
}
function pulsePrimaryGamepad(lowIntensity, highIntensity, durationMs) {
var pad = GamingShell.GamepadManager.primaryGamepad
@ -481,9 +517,7 @@ Window {
anchors.fill: parent
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Window
color: Qt.rgba(Kirigami.Theme.backgroundColor.r,
Kirigami.Theme.backgroundColor.g,
Kirigami.Theme.backgroundColor.b, 0.92)
color: MobileShell.SurfaceColors.withAlpha(MobileShell.SurfaceColors.accentSurface(Kirigami.Theme.backgroundColor, 0.24, 0.12), root.bigScreenMode ? 0.94 : 0.9)
}
FocusScope {
@ -496,8 +530,11 @@ Window {
ColumnLayout {
anchors.fill: parent
anchors.margins: Kirigami.Units.largeSpacing * 2
spacing: Kirigami.Units.largeSpacing
anchors.leftMargin: root.horizontalPadding
anchors.rightMargin: root.horizontalPadding
anchors.topMargin: root.verticalPadding
anchors.bottomMargin: root.verticalPadding
spacing: root.compactMode ? Kirigami.Units.smallSpacing : Kirigami.Units.largeSpacing
// ---- header ----
RowLayout {
@ -506,7 +543,7 @@ Window {
Kirigami.Heading {
text: i18n("Game Center")
level: 1
level: root.compactMode ? 2 : 1
}
Item { Layout.fillWidth: true }
@ -561,7 +598,7 @@ Window {
QQC2.ToolButton {
icon.name: "window-close"
text: i18n("Exit Gaming Mode")
text: root.compactMode ? i18n("Exit") : i18n("Exit Gaming Mode")
display: QQC2.AbstractButton.TextBesideIcon
Keys.onReturnPressed: clicked()
Keys.onEnterPressed: clicked()
@ -573,6 +610,7 @@ Window {
RunningGamesView {
id: runningGames
Layout.fillWidth: true
compactMode: root.compactMode
onTaskActivated: {
GamingShell.GameLauncherProvider.clearPendingLaunch()
root.gameStarted()
@ -627,7 +665,9 @@ Window {
ListView {
id: recentList
Layout.fillWidth: true
Layout.preferredHeight: Kirigami.Units.gridUnit * 5
readonly property int cardWidth: root.compactMode ? Kirigami.Units.gridUnit * 8 : Kirigami.Units.gridUnit * 10
readonly property int artHeight: Math.round(cardWidth / root.capsuleArtAspect)
Layout.preferredHeight: artHeight + Kirigami.Units.gridUnit * 1.7
orientation: ListView.Horizontal
spacing: Kirigami.Units.largeSpacing
clip: true
@ -665,7 +705,7 @@ Window {
Keys.onDownPressed: grid.forceActiveFocus()
delegate: QQC2.ItemDelegate {
width: Kirigami.Units.gridUnit * 7
width: recentList.cardWidth
height: recentList.height
required property var modelData
@ -679,16 +719,26 @@ Window {
background: Rectangle {
radius: Kirigami.Units.cornerRadius
color: parent.isCurrent
? Kirigami.Theme.highlightColor
: (parent.hovered ? Kirigami.Theme.hoverColor : "transparent")
? Qt.rgba(Kirigami.Theme.highlightColor.r, Kirigami.Theme.highlightColor.g, Kirigami.Theme.highlightColor.b, 0.22)
: (parent.hovered
? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.08)
: "transparent")
border.color: parent.isCurrent ? Kirigami.Theme.highlightColor : "transparent"
border.width: parent.isCurrent ? 2 : 0
}
contentItem: ColumnLayout {
spacing: Kirigami.Units.smallSpacing
Image {
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.preferredHeight: recentList.artHeight
radius: Kirigami.Units.cornerRadius
clip: true
color: Qt.rgba(Kirigami.Theme.alternateBackgroundColor.r, Kirigami.Theme.alternateBackgroundColor.g, Kirigami.Theme.alternateBackgroundColor.b, 0.8)
Image {
anchors.fill: parent
source: hasArt ? "file://" + modelData.artwork : ""
fillMode: Image.PreserveAspectCrop
visible: hasArt
@ -696,19 +746,20 @@ Window {
}
Kirigami.Icon {
Layout.alignment: Qt.AlignHCenter
anchors.centerIn: parent
implicitWidth: Kirigami.Units.iconSizes.large
implicitHeight: Kirigami.Units.iconSizes.large
source: modelData.icon
visible: !hasArt
}
}
PC3.Label {
Layout.fillWidth: true
text: modelData.name
maximumLineCount: 1
elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter
horizontalAlignment: Text.AlignLeft
font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.85
color: parent.parent.isCurrent
? Kirigami.Theme.highlightedTextColor
@ -736,9 +787,14 @@ Window {
}
// ---- search + filter ----
RowLayout {
Item {
Layout.fillWidth: true
spacing: Kirigami.Units.largeSpacing
implicitHeight: searchFilterStack.implicitHeight
ColumnLayout {
id: searchFilterStack
anchors.fill: parent
spacing: Kirigami.Units.smallSpacing
Kirigami.SearchField {
id: searchField
@ -758,6 +814,7 @@ Window {
QQC2.TabBar {
id: sourceFilterBar
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
Repeater {
@ -783,6 +840,7 @@ Window {
}
}
}
}
// ---- game grid ----
@ -794,11 +852,12 @@ Window {
model: GamingShell.GameLauncherProvider
readonly property real minCellSize: Kirigami.Units.gridUnit * 8
readonly property real minCellSize: root.gridMinCellSize
readonly property int columns: Math.max(2, Math.floor(width / minCellSize))
cellWidth: Math.floor(width / columns)
cellHeight: Math.floor(cellWidth * 1.5) + Kirigami.Units.gridUnit * 2
readonly property int artHeight: Math.round(cellWidth / root.capsuleArtAspect)
cellHeight: artHeight + (root.compactMode ? Kirigami.Units.gridUnit * 1.9 : Kirigami.Units.gridUnit * 2.2)
keyNavigationEnabled: true
highlightMoveDuration: 0
@ -873,7 +932,7 @@ Window {
QQC2.ItemDelegate {
anchors.fill: parent
anchors.margins: Kirigami.Units.smallSpacing
anchors.margins: root.compactMode ? 0 : Kirigami.Units.smallSpacing
padding: 0
readonly property bool isCurrent: GridView.isCurrentItem && grid.activeFocus
@ -881,24 +940,30 @@ Window {
background: Rectangle {
Kirigami.Theme.colorSet: Kirigami.Theme.Button
color: parent.isCurrent
? Kirigami.Theme.highlightColor
: (parent.hovered ? Kirigami.Theme.hoverColor : "transparent")
? Qt.rgba(Kirigami.Theme.highlightColor.r, Kirigami.Theme.highlightColor.g,
Kirigami.Theme.highlightColor.b, 0.22)
: (parent.hovered
? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g,
Kirigami.Theme.textColor.b, 0.08)
: "transparent")
radius: Kirigami.Units.cornerRadius
border.color: parent.isCurrent ? Kirigami.Theme.highlightColor : "transparent"
border.width: parent.isCurrent ? 2 : 0
}
contentItem: Item {
// ---- cover art tile ----
ColumnLayout {
anchors.fill: parent
spacing: 0
visible: hasArt
spacing: Kirigami.Units.smallSpacing
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.preferredHeight: grid.artHeight
radius: Kirigami.Units.cornerRadius
clip: true
color: "transparent"
color: Qt.rgba(Kirigami.Theme.alternateBackgroundColor.r,
Kirigami.Theme.alternateBackgroundColor.g,
Kirigami.Theme.alternateBackgroundColor.b, 0.85)
Image {
anchors.fill: parent
@ -906,6 +971,15 @@ Window {
fillMode: Image.PreserveAspectCrop
smooth: true
asynchronous: true
visible: hasArt
}
Kirigami.Icon {
anchors.centerIn: parent
implicitWidth: root.compactMode ? Kirigami.Units.iconSizes.large : Kirigami.Units.iconSizes.huge
implicitHeight: implicitWidth
source: icon
visible: !hasArt
}
Rectangle {
@ -929,76 +1003,35 @@ Window {
}
}
// Title beneath artwork
ColumnLayout {
Layout.fillWidth: true
spacing: 0
PC3.Label {
Layout.fillWidth: true
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
text: name
maximumLineCount: 1
elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignLeft
leftPadding: Kirigami.Units.smallSpacing
rightPadding: Kirigami.Units.smallSpacing
color: parent.parent.parent.isCurrent
? Kirigami.Theme.highlightedTextColor
: Kirigami.Theme.textColor
}
}
// ---- fallback icon tile ----
ColumnLayout {
anchors.fill: parent
anchors.margins: Kirigami.Units.smallSpacing
visible: !hasArt
spacing: Kirigami.Units.smallSpacing
Item { Layout.fillHeight: true }
Kirigami.Icon {
Layout.alignment: Qt.AlignHCenter
implicitWidth: Kirigami.Units.iconSizes.huge
implicitHeight: Kirigami.Units.iconSizes.huge
source: icon
scale: parent.parent.parent.isCurrent ? 1.08 : 1.0
Behavior on scale {
MobileShell.MotionNumberAnimation { type: MobileShell.Motion.EffectsFast; duration: root.shortAnimationDuration }
}
}
PC3.Label {
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
text: name
maximumLineCount: 2
wrapMode: Text.Wrap
horizontalAlignment: Text.AlignHCenter
visible: lastPlayedText.length > 0
text: lastPlayedText
maximumLineCount: 1
elide: Text.ElideRight
color: parent.parent.parent.isCurrent
? Kirigami.Theme.highlightedTextColor
: Kirigami.Theme.textColor
}
Rectangle {
Layout.alignment: Qt.AlignHCenter
visible: source !== "desktop"
radius: height / 2
color: root.sourceChipColor(source)
implicitHeight: sourceChipLabel.implicitHeight + Kirigami.Units.smallSpacing
implicitWidth: sourceChipLabel.implicitWidth + Kirigami.Units.largeSpacing
PC3.Label {
id: sourceChipLabel
anchors.centerIn: parent
text: root.sourceLabel(source)
font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.72
font.weight: Font.DemiBold
color: "white"
leftPadding: Kirigami.Units.smallSpacing
rightPadding: Kirigami.Units.smallSpacing
opacity: 0.65
font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.78
}
}
Item { Layout.fillHeight: true }
}
}
@ -1049,6 +1082,7 @@ Window {
Repeater {
model: GamingShell.GamepadManager
visible: root.bigScreenMode
RowLayout {
spacing: Kirigami.Units.smallSpacing
@ -1086,19 +1120,12 @@ Window {
// Gamepad legend
PC3.Label {
text: runningGames.activeFocus
? i18n("%1: Select %2: Close %3: Back %4: Exit %5: Settings %6: Search",
actionButtonLabel, closeButtonLabel, backButtonLabel, exitButtonLabel,
quickSettingsButtonLabel, searchButtonLabel)
: recentList.activeFocus
? i18n("%1: Play %2: Details %3: Back %4: Exit %5: Settings %6: Search",
actionButtonLabel, closeButtonLabel, backButtonLabel, exitButtonLabel,
quickSettingsButtonLabel, searchButtonLabel)
: i18n("%1: Play %2: Details %3: Back %4: Exit %5/%6: Filter %7: Settings %8: Search",
actionButtonLabel, closeButtonLabel, backButtonLabel, exitButtonLabel,
leftShoulderLabel, rightShoulderLabel, quickSettingsButtonLabel, searchButtonLabel)
font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.75
Layout.fillWidth: true
text: root.controlLegendText()
font.pointSize: Kirigami.Theme.defaultFont.pointSize * (root.compactMode ? 0.7 : 0.75)
opacity: 0.5
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignRight
}
}
}
@ -1108,6 +1135,7 @@ Window {
GamingQuickSettings {
id: quickSettings
z: 50
compactMode: root.compactMode
}
// Launch transition: brief fade to black, then dismiss

View file

@ -21,10 +21,19 @@ Item {
id: root
anchors.fill: parent
property bool compactMode: false
property bool opened: false
readonly property string acceptButtonLabel: GamingShell.GamepadManager.buttonLabel(GamingShell.GamepadManager.ButtonA)
readonly property string closeButtonLabel: GamingShell.GamepadManager.buttonLabel(GamingShell.GamepadManager.ButtonB)
function controlLegendText() {
if (GamingShell.GamepadManager.hasGamepad) {
return i18n("↕: Navigate ↔: Adjust %1: Toggle %2: Close", acceptButtonLabel, closeButtonLabel)
}
return i18n("↑↓: Navigate ←→: Adjust Enter: Toggle Esc: Close")
}
function pulsePrimaryGamepad(lowIntensity, highIntensity, durationMs) {
var pad = GamingShell.GamepadManager.primaryGamepad
if (!pad || !pad.hasRumble) {
@ -142,7 +151,9 @@ Item {
// Panel sliding in from the right
Rectangle {
id: panel
width: Math.min(root.width * 0.35, Kirigami.Units.gridUnit * 22)
width: root.compactMode
? Math.min(root.width * 0.92, Kirigami.Units.gridUnit * 26)
: Math.min(root.width * 0.35, Kirigami.Units.gridUnit * 24)
height: root.height
anchors.top: root.top
anchors.bottom: root.bottom
@ -155,9 +166,7 @@ Item {
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Window
color: Qt.rgba(Kirigami.Theme.backgroundColor.r,
Kirigami.Theme.backgroundColor.g,
Kirigami.Theme.backgroundColor.b, 0.96)
color: MobileShell.SurfaceColors.withAlpha(MobileShell.SurfaceColors.accentSurface(Kirigami.Theme.backgroundColor, 0.24, 0.12), 0.96)
// Subtle left border
Rectangle {
@ -183,7 +192,7 @@ Item {
Flickable {
anchors.fill: parent
anchors.margins: Kirigami.Units.largeSpacing * 2
anchors.margins: root.compactMode ? Kirigami.Units.largeSpacing : Kirigami.Units.largeSpacing * 2
contentHeight: settingsColumn.implicitHeight
clip: true
@ -690,11 +699,11 @@ Item {
// ---- Gamepad legend ----
PC3.Label {
Layout.fillWidth: true
text: i18n("↕: Navigate ↔: Adjust %1: Toggle %2: Close",
acceptButtonLabel, closeButtonLabel)
text: root.controlLegendText()
font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.8
opacity: 0.5
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
}
Item { Layout.fillHeight: true }

View file

@ -15,6 +15,8 @@ import plasma.applet.org.kde.plasma.mobile.homescreen.folio as Folio
Item {
id: root
property bool compactMode: false
implicitHeight: taskList.count > 0 ? column.implicitHeight : 0
readonly property bool hasTasks: taskList.count > 0
readonly property int taskCount: taskList.count
@ -73,7 +75,7 @@ Item {
visible: taskList.count > 0
Kirigami.Heading {
level: 2
level: root.compactMode ? 3 : 2
text: i18n("Running")
}
@ -120,8 +122,8 @@ Item {
required property var decoration
required property var winIdList
width: Kirigami.Units.gridUnit * 8
height: Kirigami.Units.gridUnit * 5
width: root.compactMode ? Kirigami.Units.gridUnit * 6.4 : Kirigami.Units.gridUnit * 8
height: root.compactMode ? Kirigami.Units.gridUnit * 4.2 : Kirigami.Units.gridUnit * 5
readonly property var modelIndex: tasks.makeModelIndex(index)
readonly property bool isCurrent: ListView.isCurrentItem && taskList.activeFocus

View file

@ -44,6 +44,7 @@ ContainmentItem {
// State saved when gaming mode activates, restored when it deactivates
property string _savedPowerProfile: ""
property bool _savedDnd: false
property bool _savedDynamicTiling: false
property bool _gamingSessionActive: false
function _applyGamingModeState(enabled) {
@ -57,7 +58,20 @@ ContainmentItem {
if (enabled) {
// Save current state and apply gaming optimizations
root._savedDnd = MobileShellState.ShellDBusClient.doNotDisturb
root._savedDynamicTiling = ShellSettings.Settings.dynamicTilingEnabled
root._gamingSessionActive = true
MobileShellState.ShellDBusClient.doNotDisturb = true
ShellSettings.Settings.dynamicTilingEnabled = false
if (MobileShellState.ShellDBusClient.isActionDrawerOpen) {
MobileShellState.ShellDBusClient.closeActionDrawer()
}
folio.HomeScreenState.closeFolder()
folio.HomeScreenState.closeSearchWidget()
folio.HomeScreenState.closeAppDrawer()
if (folio.HomeScreenState.viewState === Folio.HomeScreenState.SettingsView) {
folio.HomeScreenState.closeSettingsView()
}
if (GamingShell.PowerProfileControl.available) {
root._savedPowerProfile = GamingShell.PowerProfileControl.activeProfile
@ -65,17 +79,17 @@ ContainmentItem {
}
GamingShell.GameModeControl.requestStart()
root._gamingSessionActive = true
} else {
// Restore previous state
root._gamingSessionActive = false
MobileShellState.ShellDBusClient.doNotDisturb = root._savedDnd
ShellSettings.Settings.dynamicTilingEnabled = root._savedDynamicTiling
if (GamingShell.PowerProfileControl.available && root._savedPowerProfile.length > 0) {
GamingShell.PowerProfileControl.activeProfile = root._savedPowerProfile
}
GamingShell.GameModeControl.requestEnd()
root._gamingSessionActive = false
}
}
@ -251,7 +265,7 @@ ContainmentItem {
// Convergence: no scrim (popup has own background); mobile: dark scrim
color: ShellSettings.Settings.convergenceModeEnabled
? "transparent"
: Qt.rgba(Kirigami.Theme.backgroundColor.r, Kirigami.Theme.backgroundColor.g, Kirigami.Theme.backgroundColor.b, 0.46)
: MobileShell.SurfaceColors.withAlpha(MobileShell.SurfaceColors.accentSurface(Kirigami.Theme.backgroundColor, 0.18, 0.10), 0.46)
opacity: folio.HomeScreenState.appDrawerOpenProgress
}
@ -261,7 +275,7 @@ ContainmentItem {
anchors.fill: parent
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Window
color: Qt.rgba(Kirigami.Theme.backgroundColor.r, Kirigami.Theme.backgroundColor.g, Kirigami.Theme.backgroundColor.b, 0.30)
color: MobileShell.SurfaceColors.withAlpha(MobileShell.SurfaceColors.accentSurface(Kirigami.Theme.backgroundColor, 0.18, 0.10), 0.30)
opacity: folio.HomeScreenState.searchWidgetOpenProgress
}
@ -271,7 +285,7 @@ ContainmentItem {
anchors.fill: parent
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Window
color: Qt.rgba(Kirigami.Theme.backgroundColor.r, Kirigami.Theme.backgroundColor.g, Kirigami.Theme.backgroundColor.b, 0.30)
color: MobileShell.SurfaceColors.withAlpha(MobileShell.SurfaceColors.accentSurface(Kirigami.Theme.backgroundColor, 0.18, 0.10), 0.30)
opacity: folio.HomeScreenState.settingsOpenProgress
}
@ -348,7 +362,7 @@ ContainmentItem {
readonly property real rightFrameBulgeEdgeTopY: rightFrameBulgeApexY - rightFrameBulgeHalfLength
readonly property real rightFrameBulgeEdgeBottomY: rightFrameBulgeApexY + rightFrameBulgeHalfLength
readonly property real rightFrameBulgeTangent: rightFrameBulgeHalfLength * 0.55
readonly property color chromeColor: Kirigami.Theme.backgroundColor
readonly property color chromeColor: MobileShell.SurfaceColors.accentSurface(Kirigami.Theme.backgroundColor, 0.32, 0.18)
readonly property color edgeColor: Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.1)
readonly property int dockAnimationDuration: MobileShell.Motion.duration(MobileShell.Motion.SpatialDefault)
@ -1430,7 +1444,7 @@ ContainmentItem {
id: drawerSurfacePath
readonly property real cornerRadius: drawerSurface.cornerRadius
fillColor: Kirigami.Theme.backgroundColor
fillColor: MobileShell.SurfaceColors.accentSurface(Kirigami.Theme.backgroundColor, 0.32, 0.18)
strokeWidth: 0
startX: 0
startY: 0

View file

@ -4,7 +4,6 @@
import QtQuick
import QtQuick.Shapes
import org.kde.kwin.decoration
import org.kde.plasma.private.mobileshell as MobileShell
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
Decoration {
@ -24,7 +23,7 @@ Decoration {
readonly property int normalCornerRadius: 8
readonly property int cornerRadius: decoration.client.maximized ? 0 : normalCornerRadius
readonly property int frameThickness: decoration.client.maximized ? 0 : normalCornerRadius
readonly property int shortAnimationDuration: MobileShell.Motion.duration(MobileShell.Motion.EffectsFast)
readonly property int shortAnimationDuration: 120
readonly property bool windowMenuAllowed: !ShellSettings.Settings.convergenceModeEnabled
|| ShellSettings.Settings.gamingModeEnabled
|| !ShellSettings.Settings.dynamicTilingEnabled
@ -94,7 +93,7 @@ Decoration {
height: root.barHeight
radius: root.cornerRadius
color: decoration.client.active ? root.activeBar : root.inactiveBar
Behavior on color { MobileShell.MotionColorAnimation { type: MobileShell.Motion.EffectsFast; duration: root.shortAnimationDuration } }
Behavior on color { ColorAnimation { duration: root.shortAnimationDuration; easing.type: Easing.OutCubic } }
// Square off bottom half only top corners are rounded
Rectangle {
@ -135,7 +134,7 @@ Decoration {
elide: Text.ElideMiddle
horizontalAlignment: Text.AlignHCenter
renderType: Text.NativeRendering
Behavior on color { MobileShell.MotionColorAnimation { type: MobileShell.Motion.EffectsFast; duration: root.shortAnimationDuration } }
Behavior on color { ColorAnimation { duration: root.shortAnimationDuration; easing.type: Easing.OutCubic } }
}
Row {
@ -243,7 +242,7 @@ Decoration {
color: parent.pressed ? Qt.darker(parent.hoverColor, 1.3)
: parent.hovered ? parent.hoverColor
: parent.normalColor
Behavior on color { MobileShell.MotionColorAnimation { type: MobileShell.Motion.EffectsFast; duration: root.shortAnimationDuration } }
Behavior on color { ColorAnimation { duration: root.shortAnimationDuration; easing.type: Easing.OutCubic } }
Text {
anchors.centerIn: parent
@ -252,7 +251,7 @@ Decoration {
font.pixelSize: Math.round(parent.width * 0.66)
font.weight: Font.Bold
opacity: 1.0
Behavior on opacity { MobileShell.MotionNumberAnimation { type: MobileShell.Motion.EffectsFast; duration: root.shortAnimationDuration } }
Behavior on opacity { NumberAnimation { duration: root.shortAnimationDuration; easing.type: Easing.OutCubic } }
}
}
}

View file

@ -14,7 +14,6 @@
import QtQuick
import QtQuick.Layouts
import org.kde.kwin as KWinComponents
import org.kde.plasma.private.mobileshell as MobileShell
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
KWinComponents.SceneEffect {
@ -39,7 +38,7 @@ KWinComponents.SceneEffect {
readonly property int panelScreenMargin: 8
readonly property int panelCursorGap: 12
readonly property int panelCursorRightBias: 34
readonly property int hoverAnimationDuration: MobileShell.Motion.duration(MobileShell.Motion.Press)
readonly property int hoverAnimationDuration: 100
property var hoverWindowId: null
property int hoverTicks: 0
property string hoverWindowStateKey: ""
@ -651,7 +650,7 @@ KWinComponents.SceneEffect {
property bool hovered: false
Behavior on color { MobileShell.MotionColorAnimation { type: MobileShell.Motion.Press; duration: effect.hoverAnimationDuration } }
Behavior on color { ColorAnimation { duration: effect.hoverAnimationDuration; easing.type: Easing.OutCubic } }
Rectangle {
id: previewFrame

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

View file

@ -0,0 +1,2 @@
SPDX-FileCopyrightText: 2026 Marco Allegretti
SPDX-License-Identifier: EUPL-1.2

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

View file

@ -0,0 +1,2 @@
SPDX-FileCopyrightText: 2026 Marco Allegretti
SPDX-License-Identifier: EUPL-1.2

View file

@ -27,6 +27,12 @@ kcm_main=kcms/mobileshell/ui/main.qml
kcm_appearance=kcms/mobileshell/ui/AppearanceForm.qml
desktop_view=shell/contents/views/Desktop.qml
kcm_metadata=kcms/mobileshell/kcm_mobileshell.json
panel_background=components/mobileshell/qml/components/PanelBackground.qml
motion_state_layer=components/mobileshell/qml/components/MotionStateLayer.qml
surface_colors=components/mobileshell/qml/components/SurfaceColors.qml
folio_main=containments/homescreens/folio/qml/main.qml
favourites_bar=containments/homescreens/folio/qml/FavouritesBar.qml
landscape_drawer=components/mobileshell/qml/actiondrawer/private/LandscapeContentContainer.qml
require_line "$settings_header" 'Q_PROPERTY\(QString colorScheme READ colorScheme NOTIFY colorSchemeChanged\)' \
"mobile shell settings must expose the active KDE color scheme"
@ -148,6 +154,33 @@ require_line "$desktop_view" 'ShellSettings\.Settings\.applyWallpaperThemeColor\
"desktop wallpaper color extraction must feed Shift's wallpaper theme mode"
require_line "$desktop_view" 'onWallpaperThemeEnabledChanged' \
"desktop wallpaper color extraction must react when wallpaper theme mode is toggled"
require_line "$surface_colors" '!ShellSettings\.Settings\.wallpaperThemeEnabled && !ShellSettings\.Settings\.wallpaperAccentEnabled' \
"shared shell surface color helper must react to wallpaper/accent theming"
require_line "$surface_colors" 'ShellSettings\.Settings\.accentColor' \
"shared shell surface color helper must prefer the applied KDE accent color"
require_line "$surface_colors" 'ShellSettings\.Settings\.wallpaperThemeColor' \
"shared shell surface color helper must fall back to the extracted wallpaper color"
require_line "$panel_background" 'SurfaceColors\.accentSurface\(baseColor, accentTintDark, accentTintLight\)' \
"shared shell panel backgrounds must react to wallpaper/accent theming"
require_line "$surface_colors" 'mix\(base, accent\(\), darkSurface \? darkRatio : lightRatio\)' \
"shared shell panel backgrounds must tint surfaces from the active accent color"
require_line "$motion_state_layer" 'property color activeColor: SurfaceColors\.accent\(\)' \
"shared active state layers must use the same accent source as shell surfaces"
require_line "$folio_main" 'readonly property color chromeColor: MobileShell\.SurfaceColors\.accentSurface\(Kirigami\.Theme\.backgroundColor, 0\.32, 0\.18\)' \
"convergence top bar, frame, and dock chrome must tint from the active accent color"
require_line "$surface_colors" '!ShellSettings\.Settings\.wallpaperThemeEnabled && !ShellSettings\.Settings\.wallpaperAccentEnabled' \
"convergence chrome must react to wallpaper/accent theming mode"
require_line "$folio_main" 'fillColor: MobileShell\.SurfaceColors\.accentSurface\(Kirigami\.Theme\.backgroundColor, 0\.32, 0\.18\)' \
"convergence app drawer surface must tint from the active accent color"
require_line "$landscape_drawer" 'fillColor: MobileShell\.SurfaceColors\.accentSurface\(Kirigami\.Theme\.backgroundColor, 0\.32, 0\.18\)' \
"convergence action drawer surface must tint from the active accent color"
require_line "$favourites_bar" 'color: MobileShell\.SurfaceColors\.accentSurface\(Kirigami\.Theme\.backgroundColor, 0\.24, 0\.12\)' \
"dock thumbnail popup surfaces must tint from the active accent color"
require_line "$favourites_bar" 'color: leftDesktopBtn\.isCurrent \? MobileShell\.SurfaceColors\.accent\(\) : Kirigami\.Theme\.textColor' \
"dock workspace pager labels must use the same accent source as shell surfaces"
if grep -Eq -- 'Kirigami\.Theme\.highlightColor' "$favourites_bar"; then
fail "convergence dock must not use Kirigami.Theme.highlightColor directly; use MobileShell.SurfaceColors.accent()"
fi
require_line tests/CMakeLists.txt 'NAME shift-dynamic-theming' \
"dynamic theming regression test must be registered with CTest"