// SPDX-FileCopyrightText: 2026 Marco Allegretti // SPDX-License-Identifier: EUPL-1.2 import QtQuick import QtQuick.Layouts import QtQuick.Shapes 1.8 import org.kde.kirigami as Kirigami import org.kde.plasma.components 3.0 as PlasmaComponents import org.kde.plasma.private.mobileshell as MobileShell Item { id: root property int windowCount: 0 property string currentMode: "" property color surfaceColor: Kirigami.Theme.backgroundColor property int animationDuration: Kirigami.Units.shortDuration property real maxHeight: 0 signal layoutModeRequested(string mode) signal dismissRequested() readonly property int clampedWindowCount: clampedLayoutWindowCount(windowCount) readonly property var layoutOptions: layoutOptionsForWindowCount(clampedWindowCount) readonly property int optionCount: layoutOptions.length readonly property real rowHeight: Math.max(Kirigami.Units.gridUnit * 2.4, Kirigami.Units.iconSizes.medium + Kirigami.Units.smallSpacing * 2) readonly property real naturalHeight: Kirigami.Units.gridUnit * 2.2 + Math.max(1, optionCount) * rowHeight + Kirigami.Units.smallSpacing * 3 readonly property real preferredHeight: maxHeight > 0 ? Math.min(naturalHeight, maxHeight) : naturalHeight readonly property real cornerRadius: Math.min(MobileShell.Constants.convergenceWorkspaceFrameRadius, height * 0.24) clip: true function clampedLayoutWindowCount(windowCount) { const count = Math.round(Number(windowCount) || 0) return Math.max(0, Math.min(4, count)) } function linearLayoutZones(windowCount, orientation) { const count = Math.max(1, clampedLayoutWindowCount(windowCount)) let zones = [] for (let i = 0; i < count; i++) { if (orientation === "horizontal") { zones.push({ x: 0, y: i / count, w: 1, h: 1 / count }) } else { zones.push({ x: i / count, y: 0, w: 1 / count, h: 1 }) } } return zones } function masterLayoutZones(windowCount) { const count = clampedLayoutWindowCount(windowCount) if (count <= 2) { return linearLayoutZones(Math.max(1, count), "vertical") } let zones = [{ x: 0, y: 0, w: 0.58, h: 1 }] const stackCount = count - 1 for (let i = 0; i < stackCount; i++) { zones.push({ x: 0.58, y: i / stackCount, w: 0.42, h: 1 / stackCount }) } return zones } function layoutOptionsForWindowCount(windowCount) { const count = clampedLayoutWindowCount(windowCount) if (count < 2) { return [] } if (count === 2) { return [ { mode: "columns", selectedModes: ["master", "columns"], name: i18n("Side by Side"), summary: i18n("2 columns"), zones: linearLayoutZones(count, "vertical") }, { mode: "rows", selectedModes: ["rows"], name: i18n("Stacked"), summary: i18n("2 rows"), zones: linearLayoutZones(count, "horizontal") } ] } return [ { mode: "master", selectedModes: ["master"], name: i18n("Master Stack"), summary: i18n("1 + %1 stack", count - 1), zones: masterLayoutZones(count) }, { mode: "columns", selectedModes: ["columns"], name: i18n("Columns"), summary: i18n("%1 columns", count), zones: linearLayoutZones(count, "vertical") }, { mode: "rows", selectedModes: ["rows"], name: i18n("Rows"), summary: i18n("%1 rows", count), zones: linearLayoutZones(count, "horizontal") } ] } function emptyLayoutSummary() { return clampedWindowCount === 1 ? i18n("1 window") : i18n("0 windows") } Behavior on opacity { MobileShell.MotionNumberAnimation { type: MobileShell.Motion.EffectsFast duration: root.animationDuration } } Shape { anchors.fill: parent ShapePath { fillColor: root.surfaceColor strokeWidth: 0 startX: root.width startY: 0 PathLine { x: root.cornerRadius; y: 0 } PathArc { x: 0 y: root.cornerRadius radiusX: root.cornerRadius radiusY: root.cornerRadius direction: PathArc.Counterclockwise } PathLine { x: 0; y: root.height - root.cornerRadius } PathArc { x: root.cornerRadius y: root.height radiusX: root.cornerRadius radiusY: root.cornerRadius direction: PathArc.Counterclockwise } PathLine { x: root.width; y: root.height } PathLine { x: root.width; y: 0 } } } MouseArea { anchors.fill: parent hoverEnabled: true } ColumnLayout { anchors.fill: parent anchors.margins: Kirigami.Units.smallSpacing spacing: Kirigami.Units.smallSpacing PlasmaComponents.Label { Layout.fillWidth: true text: i18n("Tiling Layout") font.weight: Font.Medium elide: Text.ElideRight maximumLineCount: 1 } Repeater { model: root.layoutOptions delegate: MouseArea { id: optionButton required property var modelData Layout.fillWidth: true Layout.preferredHeight: root.rowHeight hoverEnabled: true cursorShape: Qt.PointingHandCursor readonly property bool selected: modelData.selectedModes.indexOf(root.currentMode) >= 0 onClicked: { if (!selected) { root.layoutModeRequested(modelData.mode) } root.dismissRequested() } Rectangle { anchors.fill: parent radius: Kirigami.Units.cornerRadius color: optionButton.selected ? Qt.rgba(Kirigami.Theme.highlightColor.r, Kirigami.Theme.highlightColor.g, Kirigami.Theme.highlightColor.b, 0.24) : optionButton.containsMouse ? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.08) : "transparent" border.width: optionButton.selected ? 1 : 0 border.color: Qt.rgba(Kirigami.Theme.highlightColor.r, Kirigami.Theme.highlightColor.g, Kirigami.Theme.highlightColor.b, 0.5) } RowLayout { anchors.fill: parent anchors.leftMargin: Kirigami.Units.smallSpacing anchors.rightMargin: Kirigami.Units.smallSpacing spacing: Kirigami.Units.smallSpacing Rectangle { id: layoutPreviewFrame Layout.preferredWidth: Kirigami.Units.gridUnit * 2.5 Layout.preferredHeight: Kirigami.Units.gridUnit * 1.65 radius: Kirigami.Units.cornerRadius color: Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.06) border.width: 1 border.color: Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.18) Repeater { model: optionButton.modelData.zones delegate: Rectangle { required property var modelData x: Math.round(layoutPreviewFrame.width * modelData.x) + Kirigami.Units.smallSpacing / 2 y: Math.round(layoutPreviewFrame.height * modelData.y) + Kirigami.Units.smallSpacing / 2 width: Math.max(1, Math.round(layoutPreviewFrame.width * modelData.w) - Kirigami.Units.smallSpacing) height: Math.max(1, Math.round(layoutPreviewFrame.height * modelData.h) - Kirigami.Units.smallSpacing) radius: Math.max(1, Kirigami.Units.cornerRadius - 1) color: Qt.rgba(Kirigami.Theme.highlightColor.r, Kirigami.Theme.highlightColor.g, Kirigami.Theme.highlightColor.b, 0.58) } } } ColumnLayout { Layout.fillWidth: true spacing: 0 PlasmaComponents.Label { Layout.fillWidth: true text: optionButton.modelData.name elide: Text.ElideRight maximumLineCount: 1 } PlasmaComponents.Label { Layout.fillWidth: true text: optionButton.modelData.summary opacity: 0.62 font: Kirigami.Theme.smallFont elide: Text.ElideRight maximumLineCount: 1 } } } } } ColumnLayout { Layout.fillWidth: true Layout.fillHeight: true visible: root.optionCount === 0 spacing: Kirigami.Units.smallSpacing Item { Layout.fillHeight: true } PlasmaComponents.Label { Layout.fillWidth: true horizontalAlignment: Text.AlignHCenter text: i18n("No Alternatives") font.weight: Font.Medium elide: Text.ElideRight maximumLineCount: 1 } PlasmaComponents.Label { Layout.fillWidth: true horizontalAlignment: Text.AlignHCenter text: root.emptyLayoutSummary() opacity: 0.62 font: Kirigami.Theme.smallFont elide: Text.ElideRight maximumLineCount: 1 } Item { Layout.fillHeight: true } } } }