// SPDX-FileCopyrightText: 2023 Devin Lin // 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(); } } } }