shift-shell/containments/homescreens/folio/qml/main.qml
Marco Allegretti 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

1628 lines
70 KiB
QML

// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
import QtQuick
import QtQuick.Window
import QtQuick.Layouts
import QtQuick.Effects
import QtQuick.Shapes 1.8
import org.kde.kirigami as Kirigami
import org.kde.plasma.plasmoid 2.0
import org.kde.plasma.components 3.0 as PlasmaComponents
import org.kde.plasma.private.mobileshell as MobileShell
import org.kde.plasma.private.mobileshell.state as MobileShellState
import org.kde.plasma.private.mobileshell.windowplugin as WindowPlugin
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
import org.kde.plasma.private.mobileshell.gamingshellplugin as GamingShell
import org.kde.layershell 1.0 as LayerShell
import org.kde.plasma.private.sessions 2.0
import org.kde.coreaddons as KCoreAddons
import org.kde.kcmutils as KCM
import org.kde.kirigamiaddons.components as KirigamiAddonsComponents
import plasma.applet.org.kde.plasma.mobile.homescreen.folio as Folio
import "./gaming"
import "./private"
ContainmentItem {
id: root
property var folio: root.plasmoid
readonly property int shortAnimationDuration: MobileShell.Motion.duration(MobileShell.Motion.EffectsFast)
// Tracks whether the Game Center grid is visible within gaming mode.
// If gaming mode is already enabled at startup, open it immediately so
// the user is never left without controls.
property bool gameCenterOpen: ShellSettings.Settings.gamingModeEnabled
property bool showGameCenterHint: false
// State saved when gaming mode activates, restored when it deactivates
property string _savedPowerProfile: ""
property bool _savedDnd: false
property bool _gamingSessionActive: false
function _applyGamingModeState(enabled) {
root.gameCenterOpen = enabled
GamingShell.GamepadManager.active = enabled
if (enabled === root._gamingSessionActive) {
return
}
if (enabled) {
// Save current state and apply gaming optimizations
root._savedDnd = MobileShellState.ShellDBusClient.doNotDisturb
MobileShellState.ShellDBusClient.doNotDisturb = true
if (GamingShell.PowerProfileControl.available) {
root._savedPowerProfile = GamingShell.PowerProfileControl.activeProfile
GamingShell.PowerProfileControl.activeProfile = "performance"
}
GamingShell.GameModeControl.requestStart()
root._gamingSessionActive = true
} else {
// Restore previous state
MobileShellState.ShellDBusClient.doNotDisturb = root._savedDnd
if (GamingShell.PowerProfileControl.available && root._savedPowerProfile.length > 0) {
GamingShell.PowerProfileControl.activeProfile = root._savedPowerProfile
}
GamingShell.GameModeControl.requestEnd()
root._gamingSessionActive = false
}
}
Timer {
id: gameCenterHintTimer
interval: 2600
onTriggered: root.showGameCenterHint = false
}
Connections {
target: ShellSettings.Settings
function onGamingModeEnabledChanged() {
root._applyGamingModeState(ShellSettings.Settings.gamingModeEnabled)
}
}
// Gamepad Guide button toggles Game Center overlay
Connections {
target: GamingShell.GamepadManager
enabled: ShellSettings.Settings.gamingModeEnabled
function onButtonPressed(button, gamepadIndex) {
if (button === GamingShell.GamepadManager.ButtonGuide) {
root.gameCenterOpen = !root.gameCenterOpen
}
}
}
Component.onCompleted: {
root._applyGamingModeState(ShellSettings.Settings.gamingModeEnabled)
folio.FolioSettings.load();
folio.FavouritesModel.load();
folio.PageListModel.load();
}
property MobileShell.MaskManager maskManager: MobileShell.MaskManager {
height: root.height
width: root.width
}
property MobileShell.MaskManager frontMaskManager: MobileShell.MaskManager {
height: root.height
width: root.width
}
// wallpaper blur layer
MobileShell.BlurEffect {
id: wallpaperBlur
active: folio.FolioSettings.wallpaperBlurEffect > 0
anchors.fill: parent
sourceLayer: Plasmoid.wallpaperGraphicsObject
maskSourceLayer: folio.FolioSettings.wallpaperBlurEffect > 1 ? maskManager.maskLayer : null
fullBlur: Math.min(1,
Math.max(
1 - homeScreen.contentOpacity,
// Convergence: no blur for popup drawer
ShellSettings.Settings.convergenceModeEnabled ? 0 : folio.HomeScreenState.appDrawerOpenProgress * 2,
folio.HomeScreenState.searchWidgetOpenProgress * 1.5, // blur faster during swipe
folio.HomeScreenState.folderOpenProgress
)
)
}
WindowPlugin.WindowMaximizedTracker {
id: windowMaximizedTracker
screenGeometry: Plasmoid.containment.screenGeometry
}
// In gaming mode, reopen Game Center when the last window goes away
// so the user is never stranded on a bare wallpaper.
Connections {
target: windowMaximizedTracker
enabled: ShellSettings.Settings.gamingModeEnabled
function onShowingWindowChanged() {
if (!windowMaximizedTracker.showingWindow && !root.gameCenterOpen) {
root.gameCenterOpen = true
}
}
}
// Close app drawer when a new window appears
Connections {
target: WindowPlugin.WindowUtil
function onWindowCreated() {
if (folio.HomeScreenState.viewState === Folio.HomeScreenState.AppDrawerView) {
folio.HomeScreenState.closeAppDrawer();
}
}
}
function homeAction() {
const isInWindow = (!WindowPlugin.WindowUtil.isShowingDesktop && windowMaximizedTracker.showingWindow);
// Always close action drawer
if (MobileShellState.ShellDBusClient.isActionDrawerOpen) {
MobileShellState.ShellDBusClient.closeActionDrawer();
}
if (ShellSettings.Settings.gamingModeEnabled) {
// In gaming mode Home/Menu should reopen the Game Center overlay.
root.gameCenterOpen = true;
return;
}
if (ShellSettings.Settings.convergenceModeEnabled) {
// Convergence: toggle the app drawer as a layer-shell overlay
// without disturbing open windows.
switch (folio.HomeScreenState.viewState) {
case Folio.HomeScreenState.AppDrawerView:
folio.HomeScreenState.closeAppDrawer();
break;
case Folio.HomeScreenState.FolderView:
folio.HomeScreenState.closeFolder();
break;
case Folio.HomeScreenState.SearchWidgetView:
folio.HomeScreenState.closeSearchWidget();
break;
case Folio.HomeScreenState.SettingsView:
folio.HomeScreenState.closeSettingsView();
break;
default:
folio.HomeScreenState.openAppDrawer();
break;
}
return;
}
if (isInWindow) {
folio.HomeScreenState.closeFolder();
folio.HomeScreenState.closeSearchWidget();
folio.HomeScreenState.closeAppDrawer();
folio.HomeScreenState.goToPage(0, false);
WindowPlugin.WindowUtil.minimizeAll();
// Always ensure settings view is closed
if (folio.HomeScreenState.viewState == Folio.HomeScreenState.SettingsView) {
folio.HomeScreenState.closeSettingsView();
}
} else { // If we are already on the homescreen
switch (folio.HomeScreenState.viewState) {
case Folio.HomeScreenState.PageView:
if (folio.HomeScreenState.currentPage === 0) {
folio.HomeScreenState.openAppDrawer();
} else {
folio.HomeScreenState.goToPage(0, false);
}
break;
case Folio.HomeScreenState.AppDrawerView:
folio.HomeScreenState.closeAppDrawer();
break;
case Folio.HomeScreenState.SearchWidgetView:
folio.HomeScreenState.closeSearchWidget();
break;
case Folio.HomeScreenState.FolderView:
folio.HomeScreenState.closeFolder();
break;
case Folio.HomeScreenState.SettingsView:
folio.HomeScreenState.closeSettingsView();
break;
}
}
}
Plasmoid.onActivated: homeAction()
Rectangle {
id: appDrawerBackground
anchors.fill: parent
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Window
// Convergence: no scrim (popup has own background); mobile: dark scrim
color: ShellSettings.Settings.convergenceModeEnabled
? "transparent"
: MobileShell.SurfaceColors.withAlpha(MobileShell.SurfaceColors.accentSurface(Kirigami.Theme.backgroundColor, 0.18, 0.10), 0.46)
opacity: folio.HomeScreenState.appDrawerOpenProgress
}
Rectangle {
id: searchWidgetBackground
anchors.fill: parent
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Window
color: MobileShell.SurfaceColors.withAlpha(MobileShell.SurfaceColors.accentSurface(Kirigami.Theme.backgroundColor, 0.18, 0.10), 0.30)
opacity: folio.HomeScreenState.searchWidgetOpenProgress
}
Rectangle {
id: settingsViewBackground
anchors.fill: parent
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Window
color: MobileShell.SurfaceColors.withAlpha(MobileShell.SurfaceColors.accentSurface(Kirigami.Theme.backgroundColor, 0.18, 0.10), 0.30)
opacity: folio.HomeScreenState.settingsOpenProgress
}
// Unified convergence chrome — renders the visible top bar, workspace
// frame, and dock in one mapped surface so they appear together.
// Invisible reserver surfaces in the panel/taskpanel containments still
// provide the exclusive zones that shrink KWin's MaximizeArea.
Window {
id: convergenceChrome
readonly property bool active: ShellSettings.Settings.convergenceModeEnabled
&& !ShellSettings.Settings.gamingModeEnabled
&& !folio.overviewActive
visible: active
color: "transparent"
width: Screen.width
height: Screen.height
LayerShell.Window.scope: "convergence-chrome"
LayerShell.Window.layer: LayerShell.Window.LayerTop
LayerShell.Window.anchors: LayerShell.Window.AnchorTop | LayerShell.Window.AnchorBottom
| LayerShell.Window.AnchorLeft | LayerShell.Window.AnchorRight
LayerShell.Window.exclusionZone: -1
LayerShell.Window.keyboardInteractivity: LayerShell.Window.KeyboardInteractivityOnDemand
// Auto-hide: slide dock content off-screen when a window is
// maximized. The reveal strip at the screen edge brings it back.
property real dockOffset: 0
property bool inputRegionInitialized: false
readonly property real topBarHeight: MobileShell.Constants.topPanelHeight
readonly property real dockHeight: MobileShell.Constants.convergenceDockHeight
readonly property real frameThickness: MobileShell.Constants.convergenceWorkspaceFrameThickness
readonly property real topBarHitHeight: topBarHeight + frameThickness
readonly property real frameRadius: Math.min(MobileShell.Constants.convergenceWorkspaceFrameRadius, Math.max(0, Math.min(workAreaWidth, workAreaHeight) / 2))
readonly property real workAreaX: frameThickness
readonly property real workAreaY: topBarHitHeight
readonly property real workAreaWidth: Math.max(0, width - frameThickness * 2)
readonly property real workAreaHeight: Math.max(0, height - topBarHeight - dockHeight - frameThickness * 2)
readonly property real leftEdgeHotzoneWidth: Math.max(frameThickness, Math.round(Kirigami.Units.gridUnit * 0.7))
readonly property real rightEdgeHotzoneWidth: leftEdgeHotzoneWidth
readonly property real leftLauncherWidth: Math.min(Kirigami.Units.gridUnit * 22, width * 0.42)
readonly property real leftLauncherHeight: Math.min(Kirigami.Units.gridUnit * 16, workAreaHeight * 0.66)
readonly property bool leftLauncherEnabled: root.folio.HomeScreenState.appDrawerOpenProgress <= 0
readonly property real layoutMenuWidth: Math.min(Kirigami.Units.gridUnit * 16, width * 0.34)
readonly property int layoutMenuWindowCount: Math.max(0, ShellSettings.Settings.dynamicTilingLayoutWindowCount)
readonly property bool layoutMenuEnabled: ShellSettings.Settings.dynamicTilingEnabled
&& layoutMenuWindowCount >= 2
&& root.folio.HomeScreenState.appDrawerOpenProgress <= 0
readonly property real leftFrameBulgeIdleDepth: Math.max(frameThickness * 0.45, Kirigami.Units.gridUnit * 0.16)
readonly property real leftFrameBulgeHoverDepth: 0
property real leftFrameBulgeDepth: !leftLauncherEnabled || leftLauncherOpen || leftEdgeHovered
? leftFrameBulgeHoverDepth
: leftFrameBulgeIdleDepth
property real rightFrameBulgeDepth: !layoutMenuEnabled || layoutMenuOpen || rightEdgeHovered
? leftFrameBulgeHoverDepth
: leftFrameBulgeIdleDepth
// Long, thin thickening of the lower-left workspace wall. Vertical
// tangents at all three anchors keep the curve smooth as it blends
// into the straight wall above and below.
readonly property real leftFrameBulgeEffectiveDepth: Math.max(leftFrameBulgeDepth, 0.01)
readonly property real leftFrameBulgeApexX: workAreaX + leftFrameBulgeEffectiveDepth
readonly property real leftFrameBulgeHalfLength: Kirigami.Units.gridUnit * 7.5
readonly property real leftFrameBulgeApexY: workAreaY + workAreaHeight * 0.7
readonly property real leftFrameBulgeEdgeTopY: leftFrameBulgeApexY - leftFrameBulgeHalfLength
readonly property real leftFrameBulgeEdgeBottomY: leftFrameBulgeApexY + leftFrameBulgeHalfLength
// Bezier control-handle length along the vertical tangent at each
// anchor. ~0.55 of the half-length gives a clean, taut oval profile.
readonly property real leftFrameBulgeTangent: leftFrameBulgeHalfLength * 0.55
readonly property real rightFrameBulgeEffectiveDepth: Math.max(rightFrameBulgeDepth, 0.01)
readonly property real rightFrameBulgeApexX: workAreaX + workAreaWidth - rightFrameBulgeEffectiveDepth
readonly property real rightFrameBulgeHalfLength: leftFrameBulgeHalfLength
readonly property real rightFrameBulgeApexY: leftFrameBulgeApexY
readonly property real rightFrameBulgeEdgeTopY: rightFrameBulgeApexY - rightFrameBulgeHalfLength
readonly property real rightFrameBulgeEdgeBottomY: rightFrameBulgeApexY + rightFrameBulgeHalfLength
readonly property real rightFrameBulgeTangent: rightFrameBulgeHalfLength * 0.55
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)
// Height of the input-receive strip kept at the screen edge when
// the dock is hidden. Matches the navigation panel convention.
readonly property real revealStripHeight: MobileShell.Constants.convergenceDockRevealHeight
// True once the hover-reveal timer fires; cleared on hover-exit.
property bool hoverRevealing: false
property bool leftEdgeHovered: false
property bool leftLauncherHovered: false
property bool leftLauncherOpen: false
property bool rightEdgeHovered: false
property bool layoutMenuHovered: false
property bool layoutMenuOpen: false
readonly property bool shouldHide: ShellSettings.Settings.autoHidePanelsEnabled
&& windowMaximizedTracker.showingWindow && !hoverRevealing
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Window
function updateInputRegion() {
const topBarRegion = Qt.rect(0, 0, width, topBarHitHeight)
const leftEdgeRegion = Qt.rect(0, topBarHitHeight, leftEdgeHotzoneWidth, Math.max(0, height - topBarHitHeight - dockHeight))
const rightEdgeRegion = Qt.rect(width - rightEdgeHotzoneWidth, topBarHitHeight, rightEdgeHotzoneWidth, Math.max(0, height - topBarHitHeight - dockHeight))
const leftLauncherRegion = Qt.rect(0,
Math.max(0, height - dockHeight - leftLauncherHeight),
leftLauncherWidth,
leftLauncherHeight)
const layoutMenuRegion = Qt.rect(rightLayoutMenu.x,
rightLayoutMenu.y,
rightLayoutMenu.width,
rightLayoutMenu.height)
let regions = [topBarRegion, leftEdgeRegion]
if (layoutMenuEnabled) {
regions.push(rightEdgeRegion)
}
if (shouldHide && dockOffset >= dockHeight) {
regions.push(Qt.rect(0, height - revealStripHeight, width, revealStripHeight))
} else {
regions.push(Qt.rect(0, height - dockHeight, width, dockHeight))
}
if (leftLauncherOpen) {
regions.push(leftLauncherRegion)
}
if (layoutMenuEnabled && layoutMenuOpen) {
regions.push(layoutMenuRegion)
}
MobileShell.ShellUtil.setInputRegions(convergenceChrome, regions)
}
function launchStorageId(storageId) {
if (!storageId || String(storageId).length === 0) {
return
}
var normalizedId = String(storageId)
if (!normalizedId.endsWith(".desktop")) {
normalizedId += ".desktop"
}
MobileShell.AppLaunch.launchOrActivateApp(normalizedId)
}
function refreshLeftLauncherVisibility() {
if (!leftLauncherEnabled) {
leftLauncherCloseTimer.stop()
leftEdgeHovered = false
leftLauncherHovered = false
leftLauncherOpen = false
inputRegionTimer.restart()
return
}
if (leftEdgeHovered || leftLauncherHovered) {
leftLauncherCloseTimer.stop()
leftLauncherOpen = true
} else {
leftLauncherCloseTimer.restart()
}
inputRegionTimer.restart()
}
function refreshLayoutMenuVisibility() {
if (!layoutMenuEnabled) {
layoutMenuCloseTimer.stop()
rightEdgeHovered = false
layoutMenuHovered = false
layoutMenuOpen = false
inputRegionTimer.restart()
return
}
if (rightEdgeHovered || layoutMenuHovered) {
layoutMenuCloseTimer.stop()
layoutMenuOpen = true
} else {
layoutMenuCloseTimer.restart()
}
inputRegionTimer.restart()
}
onActiveChanged: {
hoverRevealTimer.stop()
hoverRevealing = false
inputRegionInitialized = false
dockOffset = shouldHide ? dockHeight : 0
inputRegionTimer.restart()
}
onShouldHideChanged: {
if (shouldHide) {
dockOffset = dockHeight
} else {
dockOffset = 0
}
inputRegionTimer.restart()
}
onLeftLauncherEnabledChanged: refreshLeftLauncherVisibility()
onLayoutMenuEnabledChanged: refreshLayoutMenuVisibility()
// Narrow the input region to a strip at the screen edge when hidden
// so that app controls near the bottom edge are not accidentally
// intercepted. Mirrors the same pattern used by NavigationPanel.
onDockOffsetChanged: {
inputRegionTimer.restart()
}
onWidthChanged: inputRegionTimer.restart()
onHeightChanged: inputRegionTimer.restart()
onFrameSwapped: {
if (!inputRegionInitialized) {
inputRegionInitialized = true
inputRegionTimer.restart()
}
}
Timer {
id: inputRegionTimer
interval: 0
repeat: false
onTriggered: convergenceChrome.updateInputRegion()
}
// Delay reveal briefly so a quick edge graze does not pop the
// dock up mid-interaction with the underlying application.
Timer {
id: hoverRevealTimer
interval: Kirigami.Units.shortDuration
repeat: false
onTriggered: convergenceChrome.hoverRevealing = true
}
Timer {
id: leftLauncherCloseTimer
interval: Kirigami.Units.shortDuration
repeat: false
onTriggered: {
if (!convergenceChrome.leftEdgeHovered
&& !convergenceChrome.leftLauncherHovered
&& convergenceChrome.leftLauncherOpen) {
convergenceChrome.leftLauncherOpen = false
inputRegionTimer.restart()
}
}
}
Timer {
id: layoutMenuCloseTimer
interval: Kirigami.Units.shortDuration
repeat: false
onTriggered: {
if (!convergenceChrome.rightEdgeHovered
&& !convergenceChrome.layoutMenuHovered
&& convergenceChrome.layoutMenuOpen) {
convergenceChrome.layoutMenuOpen = false
inputRegionTimer.restart()
}
}
}
Behavior on dockOffset {
MobileShell.MotionNumberAnimation {
type: MobileShell.Motion.SpatialDefault
duration: convergenceChrome.dockAnimationDuration
}
}
Behavior on leftFrameBulgeDepth {
MobileShell.MotionNumberAnimation {
type: MobileShell.Motion.SpatialDefault
duration: root.shortAnimationDuration
}
}
Behavior on rightFrameBulgeDepth {
MobileShell.MotionNumberAnimation {
type: MobileShell.Motion.SpatialDefault
duration: root.shortAnimationDuration
}
}
Rectangle {
id: topBarSurface
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
height: convergenceChrome.topBarHeight
color: convergenceChrome.chromeColor
MobileShell.StatusBar {
anchors.fill: parent
showSecondRow: false
showTime: true
backgroundColor: "transparent"
}
}
Shape {
id: workspaceFrame
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: convergenceChrome.chromeColor
fillRule: ShapePath.OddEvenFill
strokeWidth: 0
startX: 0
startY: convergenceChrome.topBarHeight
PathLine { x: convergenceChrome.width; y: convergenceChrome.topBarHeight }
PathLine { x: convergenceChrome.width; y: convergenceChrome.height - convergenceChrome.dockHeight }
PathLine { x: 0; y: convergenceChrome.height - convergenceChrome.dockHeight }
PathLine { x: 0; y: convergenceChrome.topBarHeight }
PathMove { x: convergenceChrome.workAreaX + convergenceChrome.frameRadius; y: convergenceChrome.workAreaY }
PathLine { x: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth - convergenceChrome.frameRadius; y: convergenceChrome.workAreaY }
PathArc { x: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth; y: convergenceChrome.workAreaY + convergenceChrome.frameRadius; radiusX: convergenceChrome.frameRadius; radiusY: convergenceChrome.frameRadius }
PathLine { x: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth; y: convergenceChrome.rightFrameBulgeEdgeTopY }
PathCubic {
x: convergenceChrome.rightFrameBulgeApexX
y: convergenceChrome.rightFrameBulgeApexY
control1X: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth
control1Y: convergenceChrome.rightFrameBulgeEdgeTopY + convergenceChrome.rightFrameBulgeTangent
control2X: convergenceChrome.rightFrameBulgeApexX
control2Y: convergenceChrome.rightFrameBulgeApexY - convergenceChrome.rightFrameBulgeTangent
}
PathCubic {
x: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth
y: convergenceChrome.rightFrameBulgeEdgeBottomY
control1X: convergenceChrome.rightFrameBulgeApexX
control1Y: convergenceChrome.rightFrameBulgeApexY + convergenceChrome.rightFrameBulgeTangent
control2X: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth
control2Y: convergenceChrome.rightFrameBulgeEdgeBottomY - convergenceChrome.rightFrameBulgeTangent
}
PathLine { x: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth; y: convergenceChrome.workAreaY + convergenceChrome.workAreaHeight - convergenceChrome.frameRadius }
PathArc { x: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth - convergenceChrome.frameRadius; y: convergenceChrome.workAreaY + convergenceChrome.workAreaHeight; radiusX: convergenceChrome.frameRadius; radiusY: convergenceChrome.frameRadius }
PathLine { x: convergenceChrome.workAreaX + convergenceChrome.frameRadius; y: convergenceChrome.workAreaY + convergenceChrome.workAreaHeight }
PathArc { x: convergenceChrome.workAreaX; y: convergenceChrome.workAreaY + convergenceChrome.workAreaHeight - convergenceChrome.frameRadius; radiusX: convergenceChrome.frameRadius; radiusY: convergenceChrome.frameRadius }
PathLine { x: convergenceChrome.workAreaX; y: convergenceChrome.leftFrameBulgeEdgeBottomY }
PathCubic {
x: convergenceChrome.leftFrameBulgeApexX
y: convergenceChrome.leftFrameBulgeApexY
control1X: convergenceChrome.workAreaX
control1Y: convergenceChrome.leftFrameBulgeEdgeBottomY - convergenceChrome.leftFrameBulgeTangent
control2X: convergenceChrome.leftFrameBulgeApexX
control2Y: convergenceChrome.leftFrameBulgeApexY + convergenceChrome.leftFrameBulgeTangent
}
PathCubic {
x: convergenceChrome.workAreaX
y: convergenceChrome.leftFrameBulgeEdgeTopY
control1X: convergenceChrome.leftFrameBulgeApexX
control1Y: convergenceChrome.leftFrameBulgeApexY - convergenceChrome.leftFrameBulgeTangent
control2X: convergenceChrome.workAreaX
control2Y: convergenceChrome.leftFrameBulgeEdgeTopY + convergenceChrome.leftFrameBulgeTangent
}
PathLine { x: convergenceChrome.workAreaX; y: convergenceChrome.workAreaY + convergenceChrome.frameRadius }
PathArc { x: convergenceChrome.workAreaX + convergenceChrome.frameRadius; y: convergenceChrome.workAreaY; radiusX: convergenceChrome.frameRadius; radiusY: convergenceChrome.frameRadius }
}
}
Shape {
id: workspaceFrameBorder
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: "transparent"
strokeColor: convergenceChrome.edgeColor
strokeWidth: 0.85
joinStyle: ShapePath.RoundJoin
startX: convergenceChrome.workAreaX + convergenceChrome.frameRadius
startY: convergenceChrome.workAreaY
PathLine { x: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth - convergenceChrome.frameRadius; y: convergenceChrome.workAreaY }
PathQuad {
x: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth
y: convergenceChrome.workAreaY + convergenceChrome.frameRadius
controlX: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth
controlY: convergenceChrome.workAreaY
}
PathLine { x: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth; y: convergenceChrome.rightFrameBulgeEdgeTopY }
PathCubic {
x: convergenceChrome.rightFrameBulgeApexX
y: convergenceChrome.rightFrameBulgeApexY
control1X: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth
control1Y: convergenceChrome.rightFrameBulgeEdgeTopY + convergenceChrome.rightFrameBulgeTangent
control2X: convergenceChrome.rightFrameBulgeApexX
control2Y: convergenceChrome.rightFrameBulgeApexY - convergenceChrome.rightFrameBulgeTangent
}
PathCubic {
x: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth
y: convergenceChrome.rightFrameBulgeEdgeBottomY
control1X: convergenceChrome.rightFrameBulgeApexX
control1Y: convergenceChrome.rightFrameBulgeApexY + convergenceChrome.rightFrameBulgeTangent
control2X: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth
control2Y: convergenceChrome.rightFrameBulgeEdgeBottomY - convergenceChrome.rightFrameBulgeTangent
}
PathLine { x: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth; y: convergenceChrome.workAreaY + convergenceChrome.workAreaHeight - convergenceChrome.frameRadius }
PathQuad {
x: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth - convergenceChrome.frameRadius
y: convergenceChrome.workAreaY + convergenceChrome.workAreaHeight
controlX: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth
controlY: convergenceChrome.workAreaY + convergenceChrome.workAreaHeight
}
PathLine { x: convergenceChrome.workAreaX + convergenceChrome.frameRadius; y: convergenceChrome.workAreaY + convergenceChrome.workAreaHeight }
PathQuad {
x: convergenceChrome.workAreaX
y: convergenceChrome.workAreaY + convergenceChrome.workAreaHeight - convergenceChrome.frameRadius
controlX: convergenceChrome.workAreaX
controlY: convergenceChrome.workAreaY + convergenceChrome.workAreaHeight
}
PathLine { x: convergenceChrome.workAreaX; y: convergenceChrome.leftFrameBulgeEdgeBottomY }
PathCubic {
x: convergenceChrome.leftFrameBulgeApexX
y: convergenceChrome.leftFrameBulgeApexY
control1X: convergenceChrome.workAreaX
control1Y: convergenceChrome.leftFrameBulgeEdgeBottomY - convergenceChrome.leftFrameBulgeTangent
control2X: convergenceChrome.leftFrameBulgeApexX
control2Y: convergenceChrome.leftFrameBulgeApexY + convergenceChrome.leftFrameBulgeTangent
}
PathCubic {
x: convergenceChrome.workAreaX
y: convergenceChrome.leftFrameBulgeEdgeTopY
control1X: convergenceChrome.leftFrameBulgeApexX
control1Y: convergenceChrome.leftFrameBulgeApexY - convergenceChrome.leftFrameBulgeTangent
control2X: convergenceChrome.workAreaX
control2Y: convergenceChrome.leftFrameBulgeEdgeTopY + convergenceChrome.leftFrameBulgeTangent
}
PathLine { x: convergenceChrome.workAreaX; y: convergenceChrome.workAreaY + convergenceChrome.frameRadius }
PathQuad {
x: convergenceChrome.workAreaX + convergenceChrome.frameRadius
y: convergenceChrome.workAreaY
controlX: convergenceChrome.workAreaX
controlY: convergenceChrome.workAreaY
}
}
}
Rectangle {
id: dockSurface
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
height: convergenceChrome.dockHeight
color: "transparent"
HoverHandler {
id: dockHoverHandler
onHoveredChanged: {
if (hovered) {
hoverRevealTimer.start()
} else {
hoverRevealTimer.stop()
convergenceChrome.hoverRevealing = false
}
}
}
Rectangle {
anchors.fill: parent
visible: !convergenceChrome.shouldHide || convergenceChrome.dockOffset < convergenceChrome.dockHeight
color: convergenceChrome.chromeColor
}
FavouritesBar {
id: dockOverlayBar
anchors.fill: parent
folio: root.folio
maskManager: root.maskManager
homeScreen: folioHomeScreen
suppressRunningTasks: runningAppsPanel.visible
transform: Translate { y: convergenceChrome.dockOffset }
// Dock is an opaque panel — use Window colorset so all content
// (labels, hover highlights, icon tints) follows the system theme
// instead of the containment's Complementary wallpaper context.
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Window
}
}
Item {
id: leftEdgeStrip
anchors.left: parent.left
anchors.top: topBarSurface.bottom
anchors.bottom: dockSurface.top
width: convergenceChrome.leftEdgeHotzoneWidth
MouseArea {
id: leftEdgeHoverArea
anchors.fill: parent
acceptedButtons: Qt.NoButton
enabled: convergenceChrome.leftLauncherEnabled
hoverEnabled: enabled
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
onContainsMouseChanged: {
convergenceChrome.leftEdgeHovered = containsMouse
convergenceChrome.refreshLeftLauncherVisibility()
}
}
}
Item {
id: rightEdgeStrip
anchors.right: parent.right
anchors.top: topBarSurface.bottom
anchors.bottom: dockSurface.top
width: convergenceChrome.layoutMenuEnabled ? convergenceChrome.rightEdgeHotzoneWidth : 0
MouseArea {
id: rightEdgeHoverArea
anchors.fill: parent
acceptedButtons: Qt.NoButton
enabled: convergenceChrome.layoutMenuEnabled
hoverEnabled: enabled
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
onContainsMouseChanged: {
convergenceChrome.rightEdgeHovered = containsMouse
convergenceChrome.refreshLayoutMenuVisibility()
}
}
}
DynamicTilingLayoutMenu {
id: rightLayoutMenu
width: convergenceChrome.layoutMenuWidth
height: preferredHeight
x: convergenceChrome.width - width
y: convergenceChrome.height - convergenceChrome.dockHeight - height
visible: convergenceChrome.layoutMenuOpen
opacity: convergenceChrome.layoutMenuOpen ? 1 : 0
maxHeight: convergenceChrome.workAreaHeight * 0.5
windowCount: convergenceChrome.layoutMenuWindowCount
currentMode: ShellSettings.Settings.dynamicTilingLayoutMode
surfaceColor: convergenceChrome.chromeColor
animationDuration: root.shortAnimationDuration
HoverHandler {
enabled: convergenceChrome.layoutMenuOpen
onHoveredChanged: {
convergenceChrome.layoutMenuHovered = hovered
convergenceChrome.refreshLayoutMenuVisibility()
}
}
transform: Translate {
y: convergenceChrome.layoutMenuOpen ? 0 : Kirigami.Units.gridUnit
x: convergenceChrome.layoutMenuOpen ? 0 : rightLayoutMenu.width - convergenceChrome.rightEdgeHotzoneWidth
}
onLayoutModeRequested: (mode) => {
if (ShellSettings.Settings.requestDynamicTilingLayoutMode !== undefined) {
ShellSettings.Settings.requestDynamicTilingLayoutMode(mode)
}
}
onDismissRequested: {
convergenceChrome.layoutMenuOpen = false
inputRegionTimer.restart()
}
}
Item {
id: leftEdgeLauncher
width: convergenceChrome.leftLauncherWidth
height: convergenceChrome.leftLauncherHeight
x: 0
y: convergenceChrome.height - convergenceChrome.dockHeight - height
visible: convergenceChrome.leftLauncherOpen
opacity: convergenceChrome.leftLauncherOpen ? 1 : 0
clip: true
transform: Translate {
y: convergenceChrome.leftLauncherOpen ? 0 : Kirigami.Units.gridUnit
x: convergenceChrome.leftLauncherOpen ? 0 : -leftEdgeLauncher.width + convergenceChrome.leftEdgeHotzoneWidth
}
Behavior on opacity {
MobileShell.MotionNumberAnimation {
type: MobileShell.Motion.EffectsFast
duration: root.shortAnimationDuration
}
}
readonly property real cornerRadius: Math.min(MobileShell.Constants.convergenceWorkspaceFrameRadius, height * 0.24)
HoverHandler {
enabled: convergenceChrome.leftLauncherOpen
onHoveredChanged: {
convergenceChrome.leftLauncherHovered = hovered
convergenceChrome.refreshLeftLauncherVisibility()
}
}
Shape {
id: leftLauncherSurface
anchors.fill: parent
ShapePath {
fillColor: convergenceChrome.chromeColor
strokeWidth: 0
startX: 0
startY: 0
PathLine { x: leftEdgeLauncher.width - leftEdgeLauncher.cornerRadius; y: 0 }
PathArc {
x: leftEdgeLauncher.width
y: leftEdgeLauncher.cornerRadius
radiusX: leftEdgeLauncher.cornerRadius
radiusY: leftEdgeLauncher.cornerRadius
}
PathLine { x: leftEdgeLauncher.width; y: leftEdgeLauncher.height }
PathLine { x: 0; y: leftEdgeLauncher.height }
PathLine { x: 0; y: 0 }
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
}
ColumnLayout {
anchors.fill: parent
anchors.margins: Kirigami.Units.smallSpacing
spacing: Kirigami.Units.gridUnit * 0.65
ColumnLayout {
Layout.fillWidth: true
spacing: Kirigami.Units.smallSpacing
PlasmaComponents.Label {
Layout.fillWidth: true
text: i18n("Recently Used")
font.weight: Font.Medium
elide: Text.ElideRight
}
ListView {
id: recentAppsList
Layout.fillWidth: true
Layout.preferredHeight: Kirigami.Units.gridUnit * 6.8
clip: true
interactive: false
boundsBehavior: Flickable.StopAtBounds
model: folio.RecentApplicationsModel
delegate: MouseArea {
required property int index
required property var model
readonly property var delegateObject: model.delegate
readonly property var application: delegateObject ? delegateObject.application : null
readonly property bool validEntry: index < 5 && application !== null
width: recentAppsList.width
height: validEntry ? Kirigami.Units.gridUnit * 1.35 : 0
enabled: validEntry
hoverEnabled: validEntry
cursorShape: validEntry ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: {
if (application) {
convergenceChrome.launchStorageId(application.storageId)
}
}
Rectangle {
anchors.fill: parent
radius: Kirigami.Units.cornerRadius
color: parent.containsMouse
? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.08)
: "transparent"
}
RowLayout {
anchors.fill: parent
anchors.leftMargin: Kirigami.Units.smallSpacing
anchors.rightMargin: Kirigami.Units.smallSpacing
spacing: Kirigami.Units.smallSpacing
Kirigami.Icon {
Layout.preferredWidth: Kirigami.Units.iconSizes.smallMedium
Layout.preferredHeight: Layout.preferredWidth
source: application ? application.icon : ""
}
PlasmaComponents.Label {
Layout.fillWidth: true
text: application ? application.name : ""
elide: Text.ElideRight
maximumLineCount: 1
}
}
}
}
}
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
spacing: Kirigami.Units.smallSpacing
PlasmaComponents.Label {
Layout.fillWidth: true
text: i18n("Most Used")
font.weight: Font.Medium
elide: Text.ElideRight
}
ListView {
id: favouritesQuickList
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
interactive: false
boundsBehavior: Flickable.StopAtBounds
model: folio.MostUsedApplicationsModel
delegate: MouseArea {
required property int index
required property var model
readonly property var delegateObject: model.delegate
readonly property var application: delegateObject ? delegateObject.application : null
readonly property bool validEntry: index < 6 && application !== null
width: favouritesQuickList.width
height: validEntry ? Kirigami.Units.gridUnit * 1.35 : 0
enabled: validEntry
hoverEnabled: validEntry
cursorShape: validEntry ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: {
if (application) {
convergenceChrome.launchStorageId(application.storageId)
}
}
Rectangle {
anchors.fill: parent
radius: Kirigami.Units.cornerRadius
color: parent.containsMouse
? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.08)
: "transparent"
}
RowLayout {
anchors.fill: parent
anchors.leftMargin: Kirigami.Units.smallSpacing
anchors.rightMargin: Kirigami.Units.smallSpacing
spacing: Kirigami.Units.smallSpacing
Kirigami.Icon {
Layout.preferredWidth: Kirigami.Units.iconSizes.smallMedium
Layout.preferredHeight: Layout.preferredWidth
source: application ? application.icon : ""
}
PlasmaComponents.Label {
Layout.fillWidth: true
text: application ? application.name : ""
elide: Text.ElideRight
maximumLineCount: 1
}
}
}
}
}
}
}
}
// App-drawer overlay — renders the popup drawer above application
// windows in convergence mode. Same pattern as the dock overlay:
// a fullscreen layer-shell surface at LayerTop so that it appears
// over normal windows without minimizing them.
Window {
id: drawerOverlay
visible: ShellSettings.Settings.convergenceModeEnabled
&& !ShellSettings.Settings.gamingModeEnabled
&& folio.HomeScreenState.appDrawerOpenProgress > 0
color: "transparent"
width: Screen.width
height: Screen.height
LayerShell.Window.scope: "drawer-overlay"
LayerShell.Window.layer: LayerShell.Window.LayerTop
LayerShell.Window.anchors: LayerShell.Window.AnchorTop | LayerShell.Window.AnchorBottom
| LayerShell.Window.AnchorLeft | LayerShell.Window.AnchorRight
LayerShell.Window.exclusionZone: -1
LayerShell.Window.keyboardInteractivity: LayerShell.Window.KeyboardInteractivityOnDemand
// Click outside the popup to dismiss
MouseArea {
anchors.fill: parent
onClicked: folio.HomeScreenState.closeAppDrawer()
}
AppDrawer {
id: overlayDrawer
folio: root.folio
homeScreen: folioHomeScreen
readonly property real popupWidth: Math.min(Kirigami.Units.gridUnit * 28, parent.width * 0.5)
readonly property real dockHeight: MobileShell.Constants.convergenceDockHeight
readonly property real sideInset: MobileShell.Constants.convergenceWorkspaceFrameThickness
readonly property real connectedPanelGap: 0
readonly property real popupTopY: MobileShell.Constants.topPanelHeight
+ MobileShell.Constants.convergenceWorkspaceFrameThickness
readonly property real popupBottomY: parent.height
- dockHeight
- MobileShell.Constants.convergenceWorkspaceFrameThickness
readonly property real popupHeight: Math.max(0, popupBottomY - popupTopY)
width: popupWidth
height: popupHeight
opacity: folio.HomeScreenState.appDrawerOpenProgress < 0.5
? 0 : (folio.HomeScreenState.appDrawerOpenProgress - 0.5) * 2
property real animationY: (1 - folio.HomeScreenState.appDrawerOpenProgress) * (Kirigami.Units.gridUnit * 2)
x: sideInset
y: (opacity > 0)
? popupTopY + animationY
: parent.height
headerHeight: Math.round(Kirigami.Units.gridUnit * 3)
headerItem: AppDrawerHeader {
id: overlayDrawerHeader
folio: root.folio
onReleaseFocusRequested: overlayDrawer.forceActiveFocus()
}
Keys.onPressed: (event) => {
if (event.text.trim().length > 0) {
overlayDrawerHeader.addSearchText(event.text);
overlayDrawerHeader.forceActiveFocus();
event.accepted = true;
} else if (event.key === Qt.Key_Left || event.key === Qt.Key_Right
|| event.key === Qt.Key_Up || event.key === Qt.Key_Down) {
overlayDrawerHeader.forceActiveFocus();
event.accepted = true;
}
}
Connections {
target: folio.HomeScreenState
function onAppDrawerOpened() {
folio.ApplicationListSearchModel.categoryFilter = ""
overlayDrawer.forceActiveFocus()
}
}
}
CategoryPanel {
id: categoryPanel
folio: root.folio
width: Kirigami.Units.gridUnit * 9
height: overlayDrawer.popupHeight
x: overlayDrawer.x + overlayDrawer.width + overlayDrawer.connectedPanelGap
y: overlayDrawer.y
opacity: overlayDrawer.opacity
onCategorySelected: (catId) => {
folio.ApplicationListSearchModel.categoryFilter = catId
overlayDrawerHeader.clearSearchText()
}
}
Rectangle {
id: powerPanel
// Width: just enough for one icon button + side margins
readonly property real tileSize: Kirigami.Units.iconSizes.medium + 2 * Kirigami.Units.largeSpacing
width: tileSize
height: overlayDrawer.popupHeight
x: runningAppsPanel.visible
? runningAppsPanel.x + runningAppsPanel.width + overlayDrawer.connectedPanelGap
: categoryPanel.x + categoryPanel.width + overlayDrawer.connectedPanelGap
y: overlayDrawer.y
opacity: overlayDrawer.opacity
radius: 0
color: "transparent"
MouseArea {
anchors.fill: parent
}
KCoreAddons.KUser {
id: kuser
}
SessionManagement {
id: powerSession
}
// Close button anchored to top — smaller than power icons
Rectangle {
id: closeButton
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Kirigami.Units.smallSpacing
height: Kirigami.Units.iconSizes.smallMedium + 2 * Kirigami.Units.smallSpacing
radius: Kirigami.Units.cornerRadius
color: closeArea.containsPress
? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.2)
: closeArea.containsMouse
? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.1)
: "transparent"
Kirigami.Icon {
anchors.centerIn: parent
width: Kirigami.Units.iconSizes.smallMedium
height: width
source: "window-close-symbolic"
active: closeArea.containsMouse
isMask: true
color: Kirigami.Theme.textColor
}
PlasmaComponents.ToolTip {
text: i18n("Close")
visible: closeArea.containsMouse
}
MouseArea {
id: closeArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: folio.HomeScreenState.closeAppDrawer()
}
}
// Separator below close button
Rectangle {
anchors.top: closeButton.bottom
anchors.topMargin: Kirigami.Units.smallSpacing
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: Kirigami.Units.smallSpacing
anchors.rightMargin: Kirigami.Units.smallSpacing
height: 1
color: Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.15)
}
// Power buttons centred vertically in the panel
Column {
id: powerColumn
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Kirigami.Units.smallSpacing
spacing: Kirigami.Units.smallSpacing
Rectangle {
width: parent.width
height: width
radius: Kirigami.Units.cornerRadius
color: lockArea.containsPress
? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.2)
: lockArea.containsMouse
? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.1)
: "transparent"
Kirigami.Icon {
anchors.centerIn: parent
width: Kirigami.Units.iconSizes.medium
height: width
source: "system-lock-screen"
active: lockArea.containsMouse
isMask: true
color: Kirigami.Theme.textColor
}
PlasmaComponents.ToolTip {
text: i18n("Lock Screen")
visible: lockArea.containsMouse
}
MouseArea {
id: lockArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
powerSession.lock()
folio.HomeScreenState.closeAppDrawer()
}
}
}
Rectangle {
width: parent.width
height: width
radius: Kirigami.Units.cornerRadius
color: rebootArea.containsPress
? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.2)
: rebootArea.containsMouse
? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.1)
: "transparent"
Kirigami.Icon {
anchors.centerIn: parent
width: Kirigami.Units.iconSizes.medium
height: width
source: "system-reboot"
active: rebootArea.containsMouse
isMask: true
color: Kirigami.Theme.textColor
}
PlasmaComponents.ToolTip {
text: i18n("Restart")
visible: rebootArea.containsMouse
}
MouseArea {
id: rebootArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
folio.HomeScreenState.closeAppDrawer()
powerSession.requestReboot()
}
}
}
Rectangle {
width: parent.width
height: width
radius: Kirigami.Units.cornerRadius
color: shutdownArea.containsPress
? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.2)
: shutdownArea.containsMouse
? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.1)
: "transparent"
Kirigami.Icon {
anchors.centerIn: parent
width: Kirigami.Units.iconSizes.medium
height: width
source: "system-shutdown"
active: shutdownArea.containsMouse
isMask: true
color: Kirigami.Theme.textColor
}
PlasmaComponents.ToolTip {
text: i18n("Shut Down")
visible: shutdownArea.containsMouse
}
MouseArea {
id: shutdownArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
folio.HomeScreenState.closeAppDrawer()
powerSession.requestShutdown()
}
}
}
}
// Separator above user avatar
Rectangle {
anchors.bottom: userSection.top
anchors.bottomMargin: Kirigami.Units.smallSpacing
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: Kirigami.Units.smallSpacing
anchors.rightMargin: Kirigami.Units.smallSpacing
height: 1
color: Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.15)
}
// User avatar anchored to bottom — tooltip shows name, click opens user settings
Rectangle {
id: userSection
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Kirigami.Units.smallSpacing
height: width
radius: Kirigami.Units.cornerRadius
color: userArea.containsPress
? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.2)
: userArea.containsMouse
? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.1)
: "transparent"
KirigamiAddonsComponents.Avatar {
anchors.centerIn: parent
width: Kirigami.Units.iconSizes.medium
height: width
source: kuser.faceIconUrl
name: kuser.fullName || kuser.loginName
}
PlasmaComponents.ToolTip {
text: kuser.fullName || kuser.loginName
visible: userArea.containsMouse
}
MouseArea {
id: userArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
KCM.KCMLauncher.openSystemSettings("kcm_users")
folio.HomeScreenState.closeAppDrawer()
}
}
}
}
RunningAppsPanel {
id: runningAppsPanel
folio: root.folio
x: categoryPanel.x + categoryPanel.width + overlayDrawer.connectedPanelGap
y: overlayDrawer.y
width: Math.max(0, parent.width - x - powerPanel.width - overlayDrawer.sideInset - overlayDrawer.connectedPanelGap)
height: overlayDrawer.popupHeight
opacity: overlayDrawer.opacity
visible: hasTasks && opacity > 0
onTaskActivated: folio.HomeScreenState.closeAppDrawer()
}
Shape {
id: drawerSurface
x: overlayDrawer.x
y: overlayDrawer.y
width: bodyWidth + cornerRadius
height: overlayDrawer.popupHeight
opacity: overlayDrawer.opacity
visible: opacity > 0 && bodyWidth > 0 && height > 0
z: -1
preferredRendererType: Shape.CurveRenderer
asynchronous: true
enabled: false
readonly property real bodyWidth: Math.max(0, powerPanel.x + powerPanel.width - overlayDrawer.x)
readonly property real cornerRadius: Math.min(MobileShell.Constants.convergenceWorkspaceFrameRadius,
Math.max(0.01, Math.min(bodyWidth, height) / 2))
ShapePath {
id: drawerSurfacePath
readonly property real cornerRadius: drawerSurface.cornerRadius
fillColor: MobileShell.SurfaceColors.accentSurface(Kirigami.Theme.backgroundColor, 0.32, 0.18)
strokeWidth: 0
startX: 0
startY: 0
PathLine { x: drawerSurface.bodyWidth + drawerSurfacePath.cornerRadius; y: 0 }
PathArc { x: drawerSurface.bodyWidth; y: drawerSurfacePath.cornerRadius; radiusX: drawerSurfacePath.cornerRadius; radiusY: drawerSurfacePath.cornerRadius; direction: PathArc.Counterclockwise }
PathLine { x: drawerSurface.bodyWidth; y: drawerSurface.height - drawerSurfacePath.cornerRadius }
PathArc { x: drawerSurface.bodyWidth + drawerSurfacePath.cornerRadius; y: drawerSurface.height; radiusX: drawerSurfacePath.cornerRadius; radiusY: drawerSurfacePath.cornerRadius; direction: PathArc.Counterclockwise }
PathLine { x: 0; y: drawerSurface.height }
PathLine { x: 0; y: 0 }
}
}
}
// Game Center overlay — full-screen grid of games shown when gaming mode
// is active. Sits at LayerTop so it covers running application windows
// without going above system notifications.
GameCenterOverlay {
id: gameCenterOverlay
folio: root.folio
visible: ShellSettings.Settings.gamingModeEnabled && root.gameCenterOpen
onGameStarted: root.gameCenterOpen = false
onDismissRequested: {
root.gameCenterOpen = false
if (ShellSettings.Settings.gamingDismissHintEnabled) {
root.showGameCenterHint = true
gameCenterHintTimer.restart()
}
}
}
// Small persistent button at the top-right corner of the screen that lets
// the user return to the Game Center after launching a game.
// Keep the Loader active for the full duration of gaming mode so the
// opacity Behavior in GamingHUD can animate both fade-in and fade-out.
//
// Hide the HUD while a game window covers the screen. A mapped LayerShell
// surface prevents KWin from using DRM direct scanout for the fullscreen
// game window. Setting showing=false triggers the opacity fade-out and then
// sets visible=false, which unmaps the Wayland surface and lets KWin bypass
// the compositor render loop entirely for the game frame.
Loader {
active: ShellSettings.Settings.gamingModeEnabled
sourceComponent: GamingHUD {
visible: showing
showing: !root.gameCenterOpen && !windowMaximizedTracker.showingWindow
onOpenRequested: root.gameCenterOpen = true
}
}
Rectangle {
id: gameCenterHint
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: Kirigami.Units.gridUnit * 2
visible: root.showGameCenterHint && ShellSettings.Settings.gamingDismissHintEnabled
opacity: visible ? 1 : 0
z: 2000
radius: Kirigami.Units.cornerRadius
color: Qt.rgba(0, 0, 0, 0.65)
border.width: 1
border.color: Qt.rgba(1, 1, 1, 0.2)
Behavior on opacity {
MobileShell.MotionNumberAnimation { type: MobileShell.Motion.EffectsFast; duration: root.shortAnimationDuration }
}
implicitWidth: hintText.implicitWidth + Kirigami.Units.gridUnit * 2
implicitHeight: hintText.implicitHeight + Kirigami.Units.largeSpacing
PlasmaComponents.Label {
id: hintText
anchors.centerIn: parent
text: i18n("Gaming mode is still on. Use Home or the gamepad icon to reopen Game Center.")
color: "white"
wrapMode: Text.WordWrap
width: Math.min(root.width * 0.8, Kirigami.Units.gridUnit * 30)
horizontalAlignment: Text.AlignHCenter
}
}
MobileShell.HomeScreen {
id: homeScreen
anchors.fill: parent
plasmoidItem: root
onResetHomeScreenPosition: {
// NOTE: empty, because this is handled by homeAction()
}
onHomeTriggered: root.homeAction()
contentItem: Item {
// homescreen component
FolioHomeScreen {
id: folioHomeScreen
folio: root.folio
maskManager: root.maskManager
anchors.fill: parent
topMargin: homeScreen.topMargin
bottomMargin: homeScreen.bottomMargin
leftMargin: homeScreen.leftMargin
rightMargin: homeScreen.rightMargin
// Ensure is the focused item at start
Component.onCompleted: forceActiveFocus()
onWallpaperSelectorTriggered: wallpaperSelectorLoader.active = true
}
}
}
// top blur layer for items on top of the base homescreen
MobileShell.BlurEffect {
id: homescreenBlur
anchors.fill: parent
active: folio.FolioSettings.wallpaperBlurEffect > 1 && ((delegateDragItem.visible && folio.HomeScreenState.dragState.dropDelegate.type === Folio.FolioDelegate.Folder) || wallpaperSelectorLoader.active)
visible: active
fullBlur: 0
sourceLayer: homeScreenLayer
maskSourceLayer: frontMaskManager.maskLayer
// stacking both wallpaper and homescreen layers so we can blur them in one pass
Item {
id: homeScreenLayer
anchors.fill: parent
opacity: 0
// wallpaper blur
ShaderEffectSource {
anchors.fill: parent
textureSize: homescreenBlur.textureSize
sourceItem: Plasmoid.wallpaperGraphicsObject
hideSource: false
}
// homescreen blur
ShaderEffectSource {
anchors.fill: parent
textureSize: homescreenBlur.textureSize
sourceItem: homeScreen
hideSource: false
}
}
}
// drag and drop component
DelegateDragItem {
id: delegateDragItem
folio: root.folio
maskManager: root.frontMaskManager
}
// drag and drop for widgets
WidgetDragItem {
id: widgetDragItem
folio: root.folio
}
// loader for wallpaper selector
Loader {
id: wallpaperSelectorLoader
anchors.fill: parent
asynchronous: true
active: false
onLoaded: {
wallpaperSelectorLoader.item.open();
}
sourceComponent: MobileShell.WallpaperSelector {
maskManager: root.frontMaskManager
horizontal: root.width > root.height
edge: horizontal ? Qt.LeftEdge : Qt.BottomEdge
topMargin: horizontal ? folioHomeScreen.topMargin : 0
bottomMargin: horizontal ? 0 : folioHomeScreen.bottomMargin
leftMargin: horizontal ? folioHomeScreen.leftMargin : 0
rightMargin: horizontal ? folioHomeScreen.rightMargin : 0
onClosed: {
wallpaperSelectorLoader.active = false;
}
onWallpaperSettingsRequested: {
close();
folioHomeScreen.openConfigure();
}
}
}
}