// SPDX-FileCopyrightText: 2026 Marco Allegretti // SPDX-License-Identifier: EUPL-1.2 import QtQuick 2.15 import QtQuick.Layouts 1.1 import org.kde.kirigami as Kirigami import org.kde.plasma.private.mobileshell as MobileShell /** * Management/detail row shown in convergence mode. Two interaction zones: * - Left toggle pill: icon + indicator dot, tap toggles the service. * - Right detail area: name + status + chevron, tap opens detail popup. */ Item { id: root required property string text required property string status required property string icon // Visual active-state only. Shadows the built-in Item.enabled so binding it // does not disable the inner toggle/detail MouseAreas (which would make an // inactive service impossible to switch on). required property bool enabled required property var toggleFunction property bool compact: false signal detailClicked() implicitHeight: Kirigami.Units.gridUnit * (compact ? 3.1 : 3.6) Kirigami.Theme.inherit: false Kirigami.Theme.colorSet: Kirigami.Theme.Button readonly property int rowRadius: Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing readonly property color enabledBg: mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.25) readonly property color enabledBgHover: mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.32) readonly property color enabledBgPressed: mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.12) readonly property color enabledBorder: Qt.darker(Kirigami.Theme.highlightColor, 1.25) readonly property bool hasToggle: toggleFunction !== null && toggleFunction !== undefined readonly property color disabledBg: Kirigami.Theme.alternateBackgroundColor readonly property color disabledBgHover: mixColor(Kirigami.Theme.alternateBackgroundColor, Kirigami.Theme.textColor, 0.06) readonly property color disabledBgPressed: Qt.darker(disabledBg, 1.1) readonly property color disabledBorder: { let bg = Kirigami.Theme.backgroundColor; let fg = Kirigami.Theme.textColor; if (Kirigami.ColorUtils.brightnessForColor(bg) === Kirigami.ColorUtils.Light) { return Kirigami.ColorUtils.linearInterpolation(bg, fg, 0.2); } else { return Kirigami.ColorUtils.linearInterpolation(bg, fg, 0.1); } } readonly property int shortAnimationDuration: MobileShell.Motion.duration(MobileShell.Motion.EffectsFast) readonly property int pressAnimationDuration: MobileShell.Motion.duration(MobileShell.Motion.Press) readonly property real pressedScale: MobileShell.Motion.pressScaleIn function mixColor(base, overlay, ratio) { return Qt.rgba( base.r + (overlay.r - base.r) * ratio, base.g + (overlay.g - base.g) * ratio, base.b + (overlay.b - base.b) * ratio, base.a + (overlay.a - base.a) * ratio) } MobileShell.HapticsEffect { id: haptics } // ── Outer card ────────────────────────────────────────────────────── // Shadow Rectangle { anchors.top: parent.top anchors.topMargin: 1 anchors.left: parent.left anchors.right: parent.right height: parent.height radius: root.rowRadius color: Qt.rgba(0, 0, 0, root.enabled ? 0.12 : 0.08) } // Card background — always neutral base (the toggle pill carries the // enabled highlight, not the whole row). Rectangle { id: cardBg anchors.fill: parent radius: root.rowRadius border.pixelAligned: false border.width: 1 border.color: root.disabledBorder color: root.disabledBg } RowLayout { anchors.fill: parent anchors.margins: Kirigami.Units.smallSpacing spacing: Kirigami.Units.smallSpacing // ── Toggle pill (left zone) ───────────────────────────────── Item { id: togglePill Layout.alignment: Qt.AlignVCenter Layout.preferredWidth: root.height - Kirigami.Units.smallSpacing * 2 Layout.fillHeight: true Rectangle { id: pillBg anchors.fill: parent radius: Kirigami.Units.cornerRadius border.pixelAligned: false border.width: 1 border.color: root.enabled ? root.enabledBorder : root.disabledBorder color: { if (root.enabled) { if (toggleMouse.pressed) { return root.enabledBgPressed; } return toggleMouse.containsMouse ? root.enabledBgHover : root.enabledBg; } if (toggleMouse.pressed) { return root.disabledBgPressed; } return toggleMouse.containsMouse ? root.disabledBgHover : root.disabledBg; } Behavior on color { MobileShell.MotionColorAnimation { type: MobileShell.Motion.EffectsFast } } } // Scale on press property real zoomScale: toggleMouse.pressed ? root.pressedScale : 1 Behavior on zoomScale { MobileShell.MotionNumberAnimation { type: MobileShell.Motion.Press } } transform: Scale { origin.x: togglePill.width / 2 origin.y: togglePill.height / 2 xScale: togglePill.zoomScale yScale: togglePill.zoomScale } ColumnLayout { anchors.centerIn: parent spacing: Kirigami.Units.smallSpacing Kirigami.Icon { Layout.alignment: Qt.AlignHCenter implicitWidth: Kirigami.Units.iconSizes.smallMedium implicitHeight: implicitWidth source: root.icon Kirigami.Theme.inherit: false Kirigami.Theme.colorSet: Kirigami.Theme.Window isMask: true color: Kirigami.Theme.textColor } // Indicator bar Rectangle { Layout.alignment: Qt.AlignHCenter visible: root.hasToggle Layout.preferredHeight: visible ? Math.max(2, Math.round(Kirigami.Units.devicePixelRatio)) : 0 width: root.enabled ? Kirigami.Units.smallSpacing * 3 : Kirigami.Units.smallSpacing * 1.5 height: Layout.preferredHeight radius: height / 2 color: root.enabled ? Kirigami.Theme.highlightColor : Kirigami.Theme.disabledTextColor opacity: root.enabled ? 1.0 : 0.4 Behavior on width { MobileShell.MotionNumberAnimation { type: MobileShell.Motion.EffectsFast } } } } MouseArea { id: toggleMouse anchors.fill: parent hoverEnabled: true cursorShape: root.hasToggle ? Qt.PointingHandCursor : Qt.ArrowCursor onPressed: { if (root.hasToggle) { haptics.buttonVibrate() } } onClicked: { if (root.hasToggle) root.toggleFunction(); } } } // ── Detail area (right zone) ──────────────────────────────── Item { Layout.fillWidth: true Layout.fillHeight: true MobileShell.MotionStateLayer { anchors.fill: parent radius: Kirigami.Units.cornerRadius hovered: detailMouse.containsMouse pressed: detailMouse.pressed hoverOpacity: 0.03 pressedOpacity: 0.06 } MouseArea { id: detailMouse anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onPressed: haptics.buttonVibrate() onClicked: root.detailClicked() } RowLayout { anchors.fill: parent anchors.leftMargin: Kirigami.Units.smallSpacing * 2 anchors.rightMargin: Kirigami.Units.smallSpacing * 2 spacing: Kirigami.Units.smallSpacing ColumnLayout { Layout.fillWidth: true Layout.alignment: Qt.AlignVCenter spacing: 2 MobileShell.MarqueeLabel { Layout.fillWidth: true inputText: root.text font.weight: Font.Bold font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.9 } MobileShell.MarqueeLabel { Layout.fillWidth: true inputText: root.status ? root.status : (root.enabled ? i18n("On") : i18n("Off")) opacity: 0.6 font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.8 } } Kirigami.Icon { Layout.alignment: Qt.AlignVCenter implicitWidth: Kirigami.Units.iconSizes.small implicitHeight: implicitWidth source: "go-next-symbolic" Kirigami.Theme.inherit: false Kirigami.Theme.colorSet: Kirigami.Theme.Window isMask: true color: Kirigami.Theme.textColor opacity: 0.5 } } } } }