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.
This commit is contained in:
Marco Allegretti 2026-06-01 15:49:44 +02:00
parent 434f46403c
commit 75a0f7a21e

View file

@ -41,6 +41,15 @@ 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() { function controlLegendText() {
if (GamingShell.GamepadManager.hasGamepad) { if (GamingShell.GamepadManager.hasGamepad) {
@ -508,7 +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: MobileShell.SurfaceColors.withAlpha(MobileShell.SurfaceColors.accentSurface(Kirigami.Theme.backgroundColor, 0.24, 0.12), 0.92) color: MobileShell.SurfaceColors.withAlpha(MobileShell.SurfaceColors.accentSurface(Kirigami.Theme.backgroundColor, 0.24, 0.12), root.bigScreenMode ? 0.94 : 0.9)
} }
FocusScope { FocusScope {
@ -521,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 {
@ -531,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 }
@ -586,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()
@ -598,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()
@ -652,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
@ -690,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
@ -704,28 +719,39 @@ 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
source: hasArt ? "file://" + modelData.artwork : "" radius: Kirigami.Units.cornerRadius
fillMode: Image.PreserveAspectCrop clip: true
visible: hasArt color: Qt.rgba(Kirigami.Theme.alternateBackgroundColor.r, Kirigami.Theme.alternateBackgroundColor.g, Kirigami.Theme.alternateBackgroundColor.b, 0.8)
asynchronous: true
}
Kirigami.Icon { Image {
Layout.alignment: Qt.AlignHCenter anchors.fill: parent
implicitWidth: Kirigami.Units.iconSizes.large source: hasArt ? "file://" + modelData.artwork : ""
implicitHeight: Kirigami.Units.iconSizes.large fillMode: Image.PreserveAspectCrop
source: modelData.icon visible: hasArt
visible: !hasArt asynchronous: true
}
Kirigami.Icon {
anchors.centerIn: parent
implicitWidth: Kirigami.Units.iconSizes.large
implicitHeight: Kirigami.Units.iconSizes.large
source: modelData.icon
visible: !hasArt
}
} }
PC3.Label { PC3.Label {
@ -733,7 +759,7 @@ Window {
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
@ -761,49 +787,56 @@ Window {
} }
// ---- search + filter ---- // ---- search + filter ----
RowLayout { Item {
Layout.fillWidth: true Layout.fillWidth: true
spacing: Kirigami.Units.largeSpacing implicitHeight: searchFilterStack.implicitHeight
Kirigami.SearchField { ColumnLayout {
id: searchField id: searchFilterStack
Layout.fillWidth: true anchors.fill: parent
placeholderText: i18n("Search games…") spacing: Kirigami.Units.smallSpacing
onTextChanged: GamingShell.GameLauncherProvider.filterString = text
Keys.onEscapePressed: { Kirigami.SearchField {
if (text.length > 0) { id: searchField
clear() Layout.fillWidth: true
} else { placeholderText: i18n("Search games…")
root.dismissRequested() onTextChanged: GamingShell.GameLauncherProvider.filterString = text
Keys.onEscapePressed: {
if (text.length > 0) {
clear()
} else {
root.dismissRequested()
}
} }
Keys.onDownPressed: grid.forceActiveFocus()
} }
Keys.onDownPressed: grid.forceActiveFocus()
}
QQC2.TabBar { QQC2.TabBar {
id: sourceFilterBar id: sourceFilterBar
Layout.alignment: Qt.AlignVCenter Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
Repeater { Repeater {
model: { model: {
var tabs = [ var tabs = [
{label: i18n("All"), filter: ""}, {label: i18n("All"), filter: ""},
{label: i18n("Desktop"), filter: "desktop"}, {label: i18n("Desktop"), filter: "desktop"},
{label: i18n("Waydroid"),filter: "waydroid"} {label: i18n("Waydroid"),filter: "waydroid"}
] ]
if (GamingShell.GameLauncherProvider.steamAvailable) if (GamingShell.GameLauncherProvider.steamAvailable)
tabs.splice(1, 0, {label: "Steam", filter: "steam"}) tabs.splice(1, 0, {label: "Steam", filter: "steam"})
if (GamingShell.GameLauncherProvider.lutrisAvailable) if (GamingShell.GameLauncherProvider.lutrisAvailable)
tabs.push({label: "Lutris", filter: "lutris"}) tabs.push({label: "Lutris", filter: "lutris"})
if (GamingShell.GameLauncherProvider.heroicAvailable) if (GamingShell.GameLauncherProvider.heroicAvailable)
tabs.push({label: "Heroic", filter: "heroic"}) tabs.push({label: "Heroic", filter: "heroic"})
return tabs return tabs
} }
QQC2.TabButton { QQC2.TabButton {
text: modelData.label text: modelData.label
width: implicitWidth width: implicitWidth
onClicked: GamingShell.GameLauncherProvider.sourceFilter = modelData.filter onClicked: GamingShell.GameLauncherProvider.sourceFilter = modelData.filter
}
} }
} }
} }
@ -819,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
@ -898,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
@ -906,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
@ -931,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 {
@ -954,76 +1003,35 @@ Window {
} }
} }
// Title beneath artwork ColumnLayout {
PC3.Label {
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: Kirigami.Units.gridUnit * 2 spacing: 0
text: name
maximumLineCount: 1
elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
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
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 { PC3.Label {
id: sourceChipLabel Layout.fillWidth: true
anchors.centerIn: parent text: name
text: root.sourceLabel(source) maximumLineCount: 1
font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.72 elide: Text.ElideRight
font.weight: Font.DemiBold horizontalAlignment: Text.AlignLeft
color: "white" leftPadding: Kirigami.Units.smallSpacing
rightPadding: Kirigami.Units.smallSpacing
color: parent.parent.parent.isCurrent
? Kirigami.Theme.highlightedTextColor
: Kirigami.Theme.textColor
}
PC3.Label {
Layout.fillWidth: true
visible: lastPlayedText.length > 0
text: lastPlayedText
maximumLineCount: 1
elide: Text.ElideRight
leftPadding: Kirigami.Units.smallSpacing
rightPadding: Kirigami.Units.smallSpacing
opacity: 0.65
font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.78
} }
} }
Item { Layout.fillHeight: true }
} }
} }
@ -1074,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
@ -1113,7 +1122,7 @@ Window {
PC3.Label { PC3.Label {
Layout.fillWidth: true Layout.fillWidth: true
text: root.controlLegendText() text: root.controlLegendText()
font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.75 font.pointSize: Kirigami.Theme.defaultFont.pointSize * (root.compactMode ? 0.7 : 0.75)
opacity: 0.5 opacity: 0.5
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignRight horizontalAlignment: Text.AlignRight
@ -1126,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