import QtQuick import QtQuick.Effects import Quickshell import Quickshell.Wayland import Quickshell.Services.Mpris import Quickshell.Services.Pipewire import "../services" as M import "../applets" as C WlSessionLockSurface { id: root required property WlSessionLock lock required property LockAuth auth color: M.Theme.base00 property real _bgOpacity: 0 NumberAnimation on _bgOpacity { to: 1 duration: 400 easing.type: Easing.OutCubic } // Blur screenshot of desktop as background ScreencopyView { anchors.fill: parent captureSource: root.screen opacity: root._bgOpacity visible: M.Modules.lock.screenshot ?? true layer.enabled: true layer.effect: MultiEffect { autoPaddingEnabled: false blurEnabled: true blur: 1 blurMax: 64 } } // Dim overlay Rectangle { anchors.fill: parent color: Qt.rgba(M.Theme.base00.r, M.Theme.base00.g, M.Theme.base00.b, 0.4) opacity: root._bgOpacity } // Hex wave overlay C.HexWaveBackground { id: hexWave anchors.fill: parent running: root.lock.secure opacity: root._bgOpacity * 0.4 } // Keyboard input via TextInput - engages Qt's full input pipeline including // text-input protocol, which is more reliable than Keys on a plain Item in // layer-shell/lock surfaces where raw wl_keyboard delivery can be flaky. // Declared before content so it's below in z-order - content TapHandlers // receive mouse events, while keyboard activeFocus is independent of stacking. TextInput { id: _keyInput anchors.fill: parent focus: true color: "transparent" selectionColor: "transparent" selectedTextColor: "transparent" cursorVisible: false enabled: !root._unlocking && root.auth.state !== "max" && root.auth.state !== "busy" onTextChanged: if (root.auth) root.auth.buffer = text Keys.onReturnPressed: root.auth.submit() Keys.onEnterPressed: root.auth.submit() Keys.onEscapePressed: { text = ""; } onActiveFocusChanged: { if (!activeFocus) forceActiveFocus(); } } // Center content Item { id: content anchors.centerIn: parent width: 320 height: _col.height opacity: 0 scale: 0.9 NumberAnimation on opacity { to: 1 duration: 300 easing.type: Easing.OutCubic } NumberAnimation on scale { to: 1 duration: 300 easing.type: Easing.OutCubic } Column { id: _col anchors.horizontalCenter: parent.horizontalCenter spacing: 24 // Clock Text { anchors.horizontalCenter: parent.horizontalCenter text: Qt.formatTime(new Date(), "HH:mm") color: M.Theme.base05 font.pixelSize: 72 font.family: M.Theme.fontFamily font.bold: true Timer { interval: 1000 running: true repeat: true onTriggered: parent.text = Qt.formatTime(new Date(), "HH:mm") } } // Date Text { anchors.horizontalCenter: parent.horizontalCenter text: Qt.formatDate(new Date(), "dddd, d MMMM") color: M.Theme.base04 font.pixelSize: M.Theme.fontSize + 2 font.family: M.Theme.fontFamily Timer { interval: 60000 running: true repeat: true onTriggered: parent.text = Qt.formatDate(new Date(), "dddd, d MMMM") } } // Spacer Item { width: 1 height: 24 } // Password input LockInput { anchors.horizontalCenter: parent.horizontalCenter width: 280 buffer: root.auth.buffer state: root.auth.state } // Error message Text { id: _errorText anchors.horizontalCenter: parent.horizontalCenter text: root.auth.message color: M.Theme.base08 font.pixelSize: M.Theme.fontSize - 1 font.family: M.Theme.fontFamily opacity: root.auth.message ? 1 : 0 height: root.auth.message ? implicitHeight : 0 Behavior on opacity { NumberAnimation { duration: 200 } } Behavior on height { NumberAnimation { duration: 200 easing.type: Easing.OutCubic } } } // Spacer before widgets Item { width: 1 height: 8 visible: _mprisCard.visible || _volumeCard.visible } // Media widget Rectangle { id: _mprisCard anchors.horizontalCenter: parent.horizontalCenter width: 280 height: _mprisContent.implicitHeight + 16 radius: M.Theme.radius + 2 color: Qt.rgba(M.Theme.base01.r, M.Theme.base01.g, M.Theme.base01.b, 0.7) border.color: Qt.rgba(M.Theme.base03.r, M.Theme.base03.g, M.Theme.base03.b, 0.3) border.width: 1 visible: (M.Modules.lock.mpris ?? true) && _mprisPlayer !== null readonly property var _mprisPlayers: (Mpris.players.values ?? []).filter(p => p.trackTitle || p.playbackState === MprisPlaybackState.Playing || p.playbackState === MprisPlaybackState.Paused) readonly property var _mprisPlayer: _mprisPlayers[0] ?? null readonly property bool _playing: _mprisPlayer?.playbackState === MprisPlaybackState.Playing C.MprisApplet { id: _mprisContent anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top anchors.topMargin: 8 player: _mprisCard._mprisPlayer players: _mprisCard._mprisPlayers playing: _mprisCard._playing accentColor: M.Theme.base0D cachedArt: _mprisCard._mprisPlayer?.trackArtUrl ?? "" } } // Volume widget Rectangle { id: _volumeCard anchors.horizontalCenter: parent.horizontalCenter width: 280 height: _volumeContent.implicitHeight + 16 radius: M.Theme.radius + 2 color: Qt.rgba(M.Theme.base01.r, M.Theme.base01.g, M.Theme.base01.b, 0.7) border.color: Qt.rgba(M.Theme.base03.r, M.Theme.base03.g, M.Theme.base03.b, 0.3) border.width: 1 visible: (M.Modules.lock.volume ?? true) && Pipewire.defaultAudioSink !== null PwObjectTracker { objects: [Pipewire.defaultAudioSink] } C.VolumeApplet { id: _volumeContent anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top anchors.topMargin: 8 sink: Pipewire.defaultAudioSink sinkList: [] streamList: [] accentColor: M.Theme.base0E } } } } onVisibleChanged: { if (visible) _keyInput.forceActiveFocus(); } // Sync TextInput when auth clears buffer externally (PAM submit, lock reset) Connections { target: root.auth function onBufferChanged() { if (_keyInput.text !== root.auth.buffer) _keyInput.text = root.auth.buffer; } } // Unlock animation property bool _unlocking: false Connections { target: root.auth function onUnlockRequested() { root._unlocking = true; _unlockAnim.start(); } } SequentialAnimation { id: _unlockAnim ParallelAnimation { NumberAnimation { target: content property: "opacity" to: 0 duration: 200 easing.type: Easing.InCubic } NumberAnimation { target: content property: "scale" to: 0.9 duration: 200 easing.type: Easing.InCubic } NumberAnimation { target: root property: "_bgOpacity" to: 0 duration: 300 easing.type: Easing.InCubic } } PropertyAction { target: root.lock property: "locked" value: false } } // Shake animation on auth failure SequentialAnimation { id: _shakeAnim NumberAnimation { target: content property: "anchors.horizontalCenterOffset" to: 12 duration: 50 } NumberAnimation { target: content property: "anchors.horizontalCenterOffset" to: -12 duration: 50 } NumberAnimation { target: content property: "anchors.horizontalCenterOffset" to: 8 duration: 50 } NumberAnimation { target: content property: "anchors.horizontalCenterOffset" to: -8 duration: 50 } NumberAnimation { target: content property: "anchors.horizontalCenterOffset" to: 0 duration: 50 } } Connections { target: root.auth function onAuthFailed() { _shakeAnim.restart(); } } }