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
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. 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.
The upstream phone UI remains intact; convergence-specific behaviour is added on top.
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 ### What convergence mode changes
* Navigation panel replaced by a dock with running-app indicators, * The old navigation panel is replaced by a unified dock that combines
favourites, context menus, and hover tooltips pinned apps, running windows, desktop pager controls, and quick
* App drawer opens as a floating popup above the dock actions such as Home and Overview.
* The dock supports desktop workflows: pin and unpin, drag reorder,
* Window management: edge tiling, edge maximize, close buttons, task grouped window entries, middle-click close, and thumbnail previews.
context menus, Overview integration * The app drawer and action drawer run as convergence surfaces (bounded
* Status bar gains a system tray, date display, and hover highlights and shaped), with calendar, notifications, quick settings, and
* Screen space reserved for the dock via layer-shell exclusive zone desktop-focused utility actions.
* Desktop niceties: right-click wallpaper settings, minimize-all on * The status bar integrates tray/date behavior for desktop usage and is
home press, clickable page indicators 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 ### Locations
@ -39,7 +64,9 @@ cmake --install build
Compatibility-sensitive identifiers such as `org.kde.plasma.mobileshell` Compatibility-sensitive identifiers such as `org.kde.plasma.mobileshell`
are intentionally preserved in the runtime and build instructions. They are 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 ### 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. 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. 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/AppLaunch.qml
qml/components/Constants.qml qml/components/Constants.qml
qml/components/Motion.qml qml/components/Motion.qml
qml/components/SurfaceColors.qml
qml/dataproviders/AudioInfo.qml qml/dataproviders/AudioInfo.qml
qml/dataproviders/BatteryInfo.qml qml/dataproviders/BatteryInfo.qml
qml/dataproviders/BluetoothInfo.qml qml/dataproviders/BluetoothInfo.qml
@ -60,6 +61,7 @@ ecm_target_qml_sources(mobileshellplugin SOURCES
qml/components/ScreenEdgeDragEffect.qml qml/components/ScreenEdgeDragEffect.qml
qml/components/StartupFeedbackPanelFill.qml qml/components/StartupFeedbackPanelFill.qml
qml/components/StartupFeedbackWindows.qml qml/components/StartupFeedbackWindows.qml
qml/components/SurfaceColors.qml
qml/components/TextDropShadow.qml qml/components/TextDropShadow.qml
qml/components/VelocityCalculator.qml qml/components/VelocityCalculator.qml

View file

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

View file

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

View file

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

View file

@ -26,6 +26,11 @@ MobileShell.BaseItem {
required property string text required property string text
required property string status required property string status
required property string icon 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 string settingsCommand
required property var toggleFunction required property var toggleFunction

View file

@ -18,6 +18,10 @@ Item {
required property string text required property string text
required property string status required property string status
required property string icon 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 required property var toggleFunction
property bool compact: false 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 popupAnimationDuration: MobileShell.Motion.duration(MobileShell.Motion.EffectsFast)
readonly property int trayItemCount: trayList.count readonly property int trayItemCount: trayList.count
readonly property color surfaceColor: MobileShell.SurfaceColors.accentSurface(Kirigami.Theme.backgroundColor, 0.24, 0.12)
function show() { function show() {
popup.open(); popup.open();
@ -38,11 +39,11 @@ QQC2.Popup {
} }
background: Kirigami.ShadowedRectangle { background: Kirigami.ShadowedRectangle {
color: Kirigami.Theme.backgroundColor color: popup.surfaceColor
radius: Kirigami.Units.cornerRadius radius: Kirigami.Units.cornerRadius
border.color: Kirigami.ColorUtils.linearInterpolation( border.color: Kirigami.ColorUtils.linearInterpolation(
Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, 0.2) popup.surfaceColor, Kirigami.Theme.textColor, 0.2)
border.width: 1 border.width: 1
shadow.size: Kirigami.Units.gridUnit shadow.size: Kirigami.Units.gridUnit

View file

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

View file

@ -48,15 +48,24 @@ Item {
// adjust color depending on panel type // adjust color depending on panel type
property color panelColor: { property color panelColor: {
let tintPercent let tintPercent
let accentTintDark
let accentTintLight
if (panelType === PanelBackground.PanelType.Popup) { if (panelType === PanelBackground.PanelType.Popup) {
tintPercent = 0.035 tintPercent = 0.035
accentTintDark = 0.16
accentTintLight = 0.08
} else if (panelType === PanelBackground.PanelType.Base || panelType === PanelBackground.PanelType.Stacked || panelType === PanelBackground.PanelType.Flat) { } else if (panelType === PanelBackground.PanelType.Base || panelType === PanelBackground.PanelType.Stacked || panelType === PanelBackground.PanelType.Flat) {
tintPercent = 0 tintPercent = 0
accentTintDark = 0.18
accentTintLight = 0.10
} else { } else {
tintPercent = 0.06 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 // in some circumstances, panels can change there type
// for example, popup notifition when opening the popup notifition drawer // 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.colorSet: Kirigami.Theme.Complementary
Kirigami.Theme.inherit: false 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" color: popupDrawerOpened && visible ? backgroundColor : "transparent"
Behavior on color { Behavior on color {
MobileShell.MotionColorAnimation { MobileShell.MotionColorAnimation {

View file

@ -34,7 +34,7 @@ Window {
LayerShell.Window.layer: LayerShell.Window.LayerOverlay LayerShell.Window.layer: LayerShell.Window.LayerOverlay
LayerShell.Window.exclusionZone: -1 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) readonly property int overlayAnimationDuration: Math.round(MobileShell.Motion.duration(MobileShell.Motion.SpatialSlow) * 1.25)
Kirigami.Theme.colorSet: Kirigami.Theme.Complementary 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) return Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.18)
} }
if (active) { 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) { if (hovered) {
return Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.08) return Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.08)
@ -756,7 +757,7 @@ MouseArea {
PC3.Label { PC3.Label {
anchors.centerIn: parent anchors.centerIn: parent
text: (leftDesktopBtn.index + 1).toString() 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.pixelSize: Math.round(parent.height * 0.3)
font.bold: leftDesktopBtn.isCurrent font.bold: leftDesktopBtn.isCurrent
} }
@ -1487,7 +1488,7 @@ MouseArea {
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
color: Kirigami.Theme.backgroundColor color: MobileShell.SurfaceColors.accentSurface(Kirigami.Theme.backgroundColor, 0.24, 0.12)
border.color: Qt.rgba( border.color: Qt.rgba(
Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.r,
Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.g,
@ -1544,9 +1545,7 @@ MouseArea {
anchors.fill: parent anchors.fill: parent
radius: Kirigami.Units.cornerRadius radius: Kirigami.Units.cornerRadius
color: thumbEntry.containsMouse color: thumbEntry.containsMouse
? Qt.rgba(Kirigami.Theme.highlightColor.r, ? MobileShell.SurfaceColors.withAlpha(MobileShell.SurfaceColors.accent(), 0.15)
Kirigami.Theme.highlightColor.g,
Kirigami.Theme.highlightColor.b, 0.15)
: "transparent" : "transparent"
} }
@ -1790,7 +1789,7 @@ MouseArea {
width: Kirigami.Units.smallSpacing * 3 width: Kirigami.Units.smallSpacing * 3
height: Math.max(2, Math.round(Kirigami.Units.devicePixelRatio)) height: Math.max(2, Math.round(Kirigami.Units.devicePixelRatio))
radius: height / 2 radius: height / 2
color: Kirigami.Theme.highlightColor color: MobileShell.SurfaceColors.accent()
opacity: taskDelegate.dynamicTilingMaximized ? 0.95 : 0 opacity: taskDelegate.dynamicTilingMaximized ? 0.95 : 0
Behavior on opacity { Behavior on opacity {
@ -1861,7 +1860,7 @@ MouseArea {
width: taskDelegate.model.IsActive === true ? Kirigami.Units.smallSpacing * 3 : Kirigami.Units.smallSpacing * 1.5 width: taskDelegate.model.IsActive === true ? Kirigami.Units.smallSpacing * 3 : Kirigami.Units.smallSpacing * 1.5
height: Math.max(2, Math.round(Kirigami.Units.devicePixelRatio)) height: Math.max(2, Math.round(Kirigami.Units.devicePixelRatio))
radius: height / 2 radius: height / 2
color: Kirigami.Theme.highlightColor color: MobileShell.SurfaceColors.accent()
opacity: taskDelegate.model.IsActive === true ? 1.0 : 0.45 opacity: taskDelegate.model.IsActive === true ? 1.0 : 0.45
Behavior on width { Behavior on width {

View file

@ -33,6 +33,7 @@ Window {
readonly property string backButtonLabel: GamingShell.GamepadManager.buttonLabel(GamingShell.GamepadManager.ButtonB) readonly property string backButtonLabel: GamingShell.GamepadManager.buttonLabel(GamingShell.GamepadManager.ButtonB)
readonly property string closeButtonLabel: GamingShell.GamepadManager.buttonLabel(GamingShell.GamepadManager.ButtonX) readonly property string closeButtonLabel: GamingShell.GamepadManager.buttonLabel(GamingShell.GamepadManager.ButtonX)
readonly property string exitButtonLabel: GamingShell.GamepadManager.buttonLabel(GamingShell.GamepadManager.ButtonY) readonly property string exitButtonLabel: GamingShell.GamepadManager.buttonLabel(GamingShell.GamepadManager.ButtonY)
readonly property string leftShoulderLabel: GamingShell.GamepadManager.buttonLabel(GamingShell.GamepadManager.ButtonLeftShoulder) readonly property string leftShoulderLabel: GamingShell.GamepadManager.buttonLabel(GamingShell.GamepadManager.ButtonLeftShoulder)
readonly property string rightShoulderLabel: GamingShell.GamepadManager.buttonLabel(GamingShell.GamepadManager.ButtonRightShoulder) readonly property string rightShoulderLabel: GamingShell.GamepadManager.buttonLabel(GamingShell.GamepadManager.ButtonRightShoulder)
readonly property string quickSettingsButtonLabel: GamingShell.GamepadManager.buttonLabel(GamingShell.GamepadManager.ButtonBack) 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 shortAnimationDuration: MobileShell.Motion.duration(MobileShell.Motion.EffectsFast)
readonly property int longAnimationDuration: MobileShell.Motion.duration(MobileShell.Motion.EffectsDefault) readonly property int longAnimationDuration: MobileShell.Motion.duration(MobileShell.Motion.EffectsDefault)
readonly property int launchFadeDuration: MobileShell.Motion.duration(MobileShell.Motion.StandardAccel) 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) { function pulsePrimaryGamepad(lowIntensity, highIntensity, durationMs) {
var pad = GamingShell.GamepadManager.primaryGamepad var pad = GamingShell.GamepadManager.primaryGamepad
@ -481,9 +517,7 @@ Window {
anchors.fill: parent anchors.fill: parent
Kirigami.Theme.inherit: false Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Window Kirigami.Theme.colorSet: Kirigami.Theme.Window
color: Qt.rgba(Kirigami.Theme.backgroundColor.r, color: MobileShell.SurfaceColors.withAlpha(MobileShell.SurfaceColors.accentSurface(Kirigami.Theme.backgroundColor, 0.24, 0.12), root.bigScreenMode ? 0.94 : 0.9)
Kirigami.Theme.backgroundColor.g,
Kirigami.Theme.backgroundColor.b, 0.92)
} }
FocusScope { FocusScope {
@ -496,8 +530,11 @@ Window {
ColumnLayout { ColumnLayout {
anchors.fill: parent anchors.fill: parent
anchors.margins: Kirigami.Units.largeSpacing * 2 anchors.leftMargin: root.horizontalPadding
spacing: Kirigami.Units.largeSpacing anchors.rightMargin: root.horizontalPadding
anchors.topMargin: root.verticalPadding
anchors.bottomMargin: root.verticalPadding
spacing: root.compactMode ? Kirigami.Units.smallSpacing : Kirigami.Units.largeSpacing
// ---- header ---- // ---- header ----
RowLayout { RowLayout {
@ -506,7 +543,7 @@ Window {
Kirigami.Heading { Kirigami.Heading {
text: i18n("Game Center") text: i18n("Game Center")
level: 1 level: root.compactMode ? 2 : 1
} }
Item { Layout.fillWidth: true } Item { Layout.fillWidth: true }
@ -561,7 +598,7 @@ Window {
QQC2.ToolButton { QQC2.ToolButton {
icon.name: "window-close" icon.name: "window-close"
text: i18n("Exit Gaming Mode") text: root.compactMode ? i18n("Exit") : i18n("Exit Gaming Mode")
display: QQC2.AbstractButton.TextBesideIcon display: QQC2.AbstractButton.TextBesideIcon
Keys.onReturnPressed: clicked() Keys.onReturnPressed: clicked()
Keys.onEnterPressed: clicked() Keys.onEnterPressed: clicked()
@ -573,6 +610,7 @@ Window {
RunningGamesView { RunningGamesView {
id: runningGames id: runningGames
Layout.fillWidth: true Layout.fillWidth: true
compactMode: root.compactMode
onTaskActivated: { onTaskActivated: {
GamingShell.GameLauncherProvider.clearPendingLaunch() GamingShell.GameLauncherProvider.clearPendingLaunch()
root.gameStarted() root.gameStarted()
@ -627,7 +665,9 @@ Window {
ListView { ListView {
id: recentList id: recentList
Layout.fillWidth: true 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 orientation: ListView.Horizontal
spacing: Kirigami.Units.largeSpacing spacing: Kirigami.Units.largeSpacing
clip: true clip: true
@ -665,7 +705,7 @@ Window {
Keys.onDownPressed: grid.forceActiveFocus() Keys.onDownPressed: grid.forceActiveFocus()
delegate: QQC2.ItemDelegate { delegate: QQC2.ItemDelegate {
width: Kirigami.Units.gridUnit * 7 width: recentList.cardWidth
height: recentList.height height: recentList.height
required property var modelData required property var modelData
@ -679,16 +719,26 @@ Window {
background: Rectangle { background: Rectangle {
radius: Kirigami.Units.cornerRadius radius: Kirigami.Units.cornerRadius
color: parent.isCurrent color: parent.isCurrent
? Kirigami.Theme.highlightColor ? Qt.rgba(Kirigami.Theme.highlightColor.r, Kirigami.Theme.highlightColor.g, Kirigami.Theme.highlightColor.b, 0.22)
: (parent.hovered ? Kirigami.Theme.hoverColor : "transparent") : (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 { contentItem: ColumnLayout {
spacing: Kirigami.Units.smallSpacing spacing: Kirigami.Units.smallSpacing
Image { Rectangle {
Layout.fillWidth: true 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 : "" source: hasArt ? "file://" + modelData.artwork : ""
fillMode: Image.PreserveAspectCrop fillMode: Image.PreserveAspectCrop
visible: hasArt visible: hasArt
@ -696,19 +746,20 @@ Window {
} }
Kirigami.Icon { Kirigami.Icon {
Layout.alignment: Qt.AlignHCenter anchors.centerIn: parent
implicitWidth: Kirigami.Units.iconSizes.large implicitWidth: Kirigami.Units.iconSizes.large
implicitHeight: Kirigami.Units.iconSizes.large implicitHeight: Kirigami.Units.iconSizes.large
source: modelData.icon source: modelData.icon
visible: !hasArt visible: !hasArt
} }
}
PC3.Label { PC3.Label {
Layout.fillWidth: true Layout.fillWidth: true
text: modelData.name text: modelData.name
maximumLineCount: 1 maximumLineCount: 1
elide: Text.ElideRight elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignLeft
font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.85 font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.85
color: parent.parent.isCurrent color: parent.parent.isCurrent
? Kirigami.Theme.highlightedTextColor ? Kirigami.Theme.highlightedTextColor
@ -736,9 +787,14 @@ Window {
} }
// ---- search + filter ---- // ---- search + filter ----
RowLayout { Item {
Layout.fillWidth: true Layout.fillWidth: true
spacing: Kirigami.Units.largeSpacing implicitHeight: searchFilterStack.implicitHeight
ColumnLayout {
id: searchFilterStack
anchors.fill: parent
spacing: Kirigami.Units.smallSpacing
Kirigami.SearchField { Kirigami.SearchField {
id: searchField id: searchField
@ -758,6 +814,7 @@ Window {
QQC2.TabBar { QQC2.TabBar {
id: sourceFilterBar id: sourceFilterBar
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
Repeater { Repeater {
@ -783,6 +840,7 @@ Window {
} }
} }
} }
}
// ---- game grid ---- // ---- game grid ----
@ -794,11 +852,12 @@ Window {
model: GamingShell.GameLauncherProvider 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)) readonly property int columns: Math.max(2, Math.floor(width / minCellSize))
cellWidth: Math.floor(width / columns) 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 keyNavigationEnabled: true
highlightMoveDuration: 0 highlightMoveDuration: 0
@ -873,7 +932,7 @@ Window {
QQC2.ItemDelegate { QQC2.ItemDelegate {
anchors.fill: parent anchors.fill: parent
anchors.margins: Kirigami.Units.smallSpacing anchors.margins: root.compactMode ? 0 : Kirigami.Units.smallSpacing
padding: 0 padding: 0
readonly property bool isCurrent: GridView.isCurrentItem && grid.activeFocus readonly property bool isCurrent: GridView.isCurrentItem && grid.activeFocus
@ -881,24 +940,30 @@ Window {
background: Rectangle { background: Rectangle {
Kirigami.Theme.colorSet: Kirigami.Theme.Button Kirigami.Theme.colorSet: Kirigami.Theme.Button
color: parent.isCurrent color: parent.isCurrent
? Kirigami.Theme.highlightColor ? Qt.rgba(Kirigami.Theme.highlightColor.r, Kirigami.Theme.highlightColor.g,
: (parent.hovered ? Kirigami.Theme.hoverColor : "transparent") 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 radius: Kirigami.Units.cornerRadius
border.color: parent.isCurrent ? Kirigami.Theme.highlightColor : "transparent"
border.width: parent.isCurrent ? 2 : 0
} }
contentItem: Item { contentItem: Item {
// ---- cover art tile ----
ColumnLayout { ColumnLayout {
anchors.fill: parent anchors.fill: parent
spacing: 0 spacing: Kirigami.Units.smallSpacing
visible: hasArt
Rectangle { Rectangle {
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.preferredHeight: grid.artHeight
radius: Kirigami.Units.cornerRadius radius: Kirigami.Units.cornerRadius
clip: true clip: true
color: "transparent" color: Qt.rgba(Kirigami.Theme.alternateBackgroundColor.r,
Kirigami.Theme.alternateBackgroundColor.g,
Kirigami.Theme.alternateBackgroundColor.b, 0.85)
Image { Image {
anchors.fill: parent anchors.fill: parent
@ -906,6 +971,15 @@ Window {
fillMode: Image.PreserveAspectCrop fillMode: Image.PreserveAspectCrop
smooth: true smooth: true
asynchronous: 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 { Rectangle {
@ -929,76 +1003,35 @@ Window {
} }
} }
// Title beneath artwork ColumnLayout {
Layout.fillWidth: true
spacing: 0
PC3.Label { PC3.Label {
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
text: name text: name
maximumLineCount: 1 maximumLineCount: 1
elide: Text.ElideRight elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignLeft
verticalAlignment: Text.AlignVCenter
leftPadding: Kirigami.Units.smallSpacing leftPadding: Kirigami.Units.smallSpacing
rightPadding: Kirigami.Units.smallSpacing rightPadding: Kirigami.Units.smallSpacing
color: parent.parent.parent.isCurrent color: parent.parent.parent.isCurrent
? Kirigami.Theme.highlightedTextColor ? Kirigami.Theme.highlightedTextColor
: Kirigami.Theme.textColor : 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 { PC3.Label {
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true Layout.fillWidth: true
text: name visible: lastPlayedText.length > 0
maximumLineCount: 2 text: lastPlayedText
wrapMode: Text.Wrap maximumLineCount: 1
horizontalAlignment: Text.AlignHCenter
elide: Text.ElideRight elide: Text.ElideRight
color: parent.parent.parent.isCurrent leftPadding: Kirigami.Units.smallSpacing
? Kirigami.Theme.highlightedTextColor rightPadding: Kirigami.Units.smallSpacing
: Kirigami.Theme.textColor opacity: 0.65
} font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.78
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"
} }
} }
Item { Layout.fillHeight: true }
} }
} }
@ -1049,6 +1082,7 @@ Window {
Repeater { Repeater {
model: GamingShell.GamepadManager model: GamingShell.GamepadManager
visible: root.bigScreenMode
RowLayout { RowLayout {
spacing: Kirigami.Units.smallSpacing spacing: Kirigami.Units.smallSpacing
@ -1086,19 +1120,12 @@ Window {
// Gamepad legend // Gamepad legend
PC3.Label { PC3.Label {
text: runningGames.activeFocus Layout.fillWidth: true
? i18n("%1: Select %2: Close %3: Back %4: Exit %5: Settings %6: Search", text: root.controlLegendText()
actionButtonLabel, closeButtonLabel, backButtonLabel, exitButtonLabel, font.pointSize: Kirigami.Theme.defaultFont.pointSize * (root.compactMode ? 0.7 : 0.75)
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
opacity: 0.5 opacity: 0.5
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignRight
} }
} }
} }
@ -1108,6 +1135,7 @@ Window {
GamingQuickSettings { GamingQuickSettings {
id: quickSettings id: quickSettings
z: 50 z: 50
compactMode: root.compactMode
} }
// Launch transition: brief fade to black, then dismiss // Launch transition: brief fade to black, then dismiss

View file

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

View file

@ -15,6 +15,8 @@ import plasma.applet.org.kde.plasma.mobile.homescreen.folio as Folio
Item { Item {
id: root id: root
property bool compactMode: false
implicitHeight: taskList.count > 0 ? column.implicitHeight : 0 implicitHeight: taskList.count > 0 ? column.implicitHeight : 0
readonly property bool hasTasks: taskList.count > 0 readonly property bool hasTasks: taskList.count > 0
readonly property int taskCount: taskList.count readonly property int taskCount: taskList.count
@ -73,7 +75,7 @@ Item {
visible: taskList.count > 0 visible: taskList.count > 0
Kirigami.Heading { Kirigami.Heading {
level: 2 level: root.compactMode ? 3 : 2
text: i18n("Running") text: i18n("Running")
} }
@ -120,8 +122,8 @@ Item {
required property var decoration required property var decoration
required property var winIdList required property var winIdList
width: Kirigami.Units.gridUnit * 8 width: root.compactMode ? Kirigami.Units.gridUnit * 6.4 : Kirigami.Units.gridUnit * 8
height: Kirigami.Units.gridUnit * 5 height: root.compactMode ? Kirigami.Units.gridUnit * 4.2 : Kirigami.Units.gridUnit * 5
readonly property var modelIndex: tasks.makeModelIndex(index) readonly property var modelIndex: tasks.makeModelIndex(index)
readonly property bool isCurrent: ListView.isCurrentItem && taskList.activeFocus 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 // State saved when gaming mode activates, restored when it deactivates
property string _savedPowerProfile: "" property string _savedPowerProfile: ""
property bool _savedDnd: false property bool _savedDnd: false
property bool _savedDynamicTiling: false
property bool _gamingSessionActive: false property bool _gamingSessionActive: false
function _applyGamingModeState(enabled) { function _applyGamingModeState(enabled) {
@ -57,7 +58,20 @@ ContainmentItem {
if (enabled) { if (enabled) {
// Save current state and apply gaming optimizations // Save current state and apply gaming optimizations
root._savedDnd = MobileShellState.ShellDBusClient.doNotDisturb root._savedDnd = MobileShellState.ShellDBusClient.doNotDisturb
root._savedDynamicTiling = ShellSettings.Settings.dynamicTilingEnabled
root._gamingSessionActive = true
MobileShellState.ShellDBusClient.doNotDisturb = 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) { if (GamingShell.PowerProfileControl.available) {
root._savedPowerProfile = GamingShell.PowerProfileControl.activeProfile root._savedPowerProfile = GamingShell.PowerProfileControl.activeProfile
@ -65,17 +79,17 @@ ContainmentItem {
} }
GamingShell.GameModeControl.requestStart() GamingShell.GameModeControl.requestStart()
root._gamingSessionActive = true
} else { } else {
// Restore previous state // Restore previous state
root._gamingSessionActive = false
MobileShellState.ShellDBusClient.doNotDisturb = root._savedDnd MobileShellState.ShellDBusClient.doNotDisturb = root._savedDnd
ShellSettings.Settings.dynamicTilingEnabled = root._savedDynamicTiling
if (GamingShell.PowerProfileControl.available && root._savedPowerProfile.length > 0) { if (GamingShell.PowerProfileControl.available && root._savedPowerProfile.length > 0) {
GamingShell.PowerProfileControl.activeProfile = root._savedPowerProfile GamingShell.PowerProfileControl.activeProfile = root._savedPowerProfile
} }
GamingShell.GameModeControl.requestEnd() GamingShell.GameModeControl.requestEnd()
root._gamingSessionActive = false
} }
} }
@ -251,7 +265,7 @@ ContainmentItem {
// Convergence: no scrim (popup has own background); mobile: dark scrim // Convergence: no scrim (popup has own background); mobile: dark scrim
color: ShellSettings.Settings.convergenceModeEnabled color: ShellSettings.Settings.convergenceModeEnabled
? "transparent" ? "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 opacity: folio.HomeScreenState.appDrawerOpenProgress
} }
@ -261,7 +275,7 @@ ContainmentItem {
anchors.fill: parent anchors.fill: parent
Kirigami.Theme.inherit: false Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Window 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 opacity: folio.HomeScreenState.searchWidgetOpenProgress
} }
@ -271,7 +285,7 @@ ContainmentItem {
anchors.fill: parent anchors.fill: parent
Kirigami.Theme.inherit: false Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Window 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 opacity: folio.HomeScreenState.settingsOpenProgress
} }
@ -348,7 +362,7 @@ ContainmentItem {
readonly property real rightFrameBulgeEdgeTopY: rightFrameBulgeApexY - rightFrameBulgeHalfLength readonly property real rightFrameBulgeEdgeTopY: rightFrameBulgeApexY - rightFrameBulgeHalfLength
readonly property real rightFrameBulgeEdgeBottomY: rightFrameBulgeApexY + rightFrameBulgeHalfLength readonly property real rightFrameBulgeEdgeBottomY: rightFrameBulgeApexY + rightFrameBulgeHalfLength
readonly property real rightFrameBulgeTangent: rightFrameBulgeHalfLength * 0.55 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 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) readonly property int dockAnimationDuration: MobileShell.Motion.duration(MobileShell.Motion.SpatialDefault)
@ -1430,7 +1444,7 @@ ContainmentItem {
id: drawerSurfacePath id: drawerSurfacePath
readonly property real cornerRadius: drawerSurface.cornerRadius readonly property real cornerRadius: drawerSurface.cornerRadius
fillColor: Kirigami.Theme.backgroundColor fillColor: MobileShell.SurfaceColors.accentSurface(Kirigami.Theme.backgroundColor, 0.32, 0.18)
strokeWidth: 0 strokeWidth: 0
startX: 0 startX: 0
startY: 0 startY: 0

View file

@ -4,7 +4,6 @@
import QtQuick import QtQuick
import QtQuick.Shapes import QtQuick.Shapes
import org.kde.kwin.decoration import org.kde.kwin.decoration
import org.kde.plasma.private.mobileshell as MobileShell
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
Decoration { Decoration {
@ -24,7 +23,7 @@ Decoration {
readonly property int normalCornerRadius: 8 readonly property int normalCornerRadius: 8
readonly property int cornerRadius: decoration.client.maximized ? 0 : normalCornerRadius readonly property int cornerRadius: decoration.client.maximized ? 0 : normalCornerRadius
readonly property int frameThickness: 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 readonly property bool windowMenuAllowed: !ShellSettings.Settings.convergenceModeEnabled
|| ShellSettings.Settings.gamingModeEnabled || ShellSettings.Settings.gamingModeEnabled
|| !ShellSettings.Settings.dynamicTilingEnabled || !ShellSettings.Settings.dynamicTilingEnabled
@ -94,7 +93,7 @@ Decoration {
height: root.barHeight height: root.barHeight
radius: root.cornerRadius radius: root.cornerRadius
color: decoration.client.active ? root.activeBar : root.inactiveBar 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 // Square off bottom half only top corners are rounded
Rectangle { Rectangle {
@ -135,7 +134,7 @@ Decoration {
elide: Text.ElideMiddle elide: Text.ElideMiddle
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
renderType: Text.NativeRendering 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 { Row {
@ -243,7 +242,7 @@ Decoration {
color: parent.pressed ? Qt.darker(parent.hoverColor, 1.3) color: parent.pressed ? Qt.darker(parent.hoverColor, 1.3)
: parent.hovered ? parent.hoverColor : parent.hovered ? parent.hoverColor
: parent.normalColor : 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 { Text {
anchors.centerIn: parent anchors.centerIn: parent
@ -252,7 +251,7 @@ Decoration {
font.pixelSize: Math.round(parent.width * 0.66) font.pixelSize: Math.round(parent.width * 0.66)
font.weight: Font.Bold font.weight: Font.Bold
opacity: 1.0 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
import QtQuick.Layouts import QtQuick.Layouts
import org.kde.kwin as KWinComponents import org.kde.kwin as KWinComponents
import org.kde.plasma.private.mobileshell as MobileShell
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
KWinComponents.SceneEffect { KWinComponents.SceneEffect {
@ -39,7 +38,7 @@ KWinComponents.SceneEffect {
readonly property int panelScreenMargin: 8 readonly property int panelScreenMargin: 8
readonly property int panelCursorGap: 12 readonly property int panelCursorGap: 12
readonly property int panelCursorRightBias: 34 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 var hoverWindowId: null
property int hoverTicks: 0 property int hoverTicks: 0
property string hoverWindowStateKey: "" property string hoverWindowStateKey: ""
@ -651,7 +650,7 @@ KWinComponents.SceneEffect {
property bool hovered: false 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 { Rectangle {
id: previewFrame 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 kcm_appearance=kcms/mobileshell/ui/AppearanceForm.qml
desktop_view=shell/contents/views/Desktop.qml desktop_view=shell/contents/views/Desktop.qml
kcm_metadata=kcms/mobileshell/kcm_mobileshell.json 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\)' \ require_line "$settings_header" 'Q_PROPERTY\(QString colorScheme READ colorScheme NOTIFY colorSchemeChanged\)' \
"mobile shell settings must expose the active KDE color scheme" "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" "desktop wallpaper color extraction must feed Shift's wallpaper theme mode"
require_line "$desktop_view" 'onWallpaperThemeEnabledChanged' \ require_line "$desktop_view" 'onWallpaperThemeEnabledChanged' \
"desktop wallpaper color extraction must react when wallpaper theme mode is toggled" "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' \ require_line tests/CMakeLists.txt 'NAME shift-dynamic-theming' \
"dynamic theming regression test must be registered with CTest" "dynamic theming regression test must be registered with CTest"