nova-shell/shell/lock/LockSurface.qml

358 lines
9.6 KiB
QML

import QtQuick
import QtQuick.Effects
import Quickshell
import Quickshell.Wayland
import "../services" as S
import "../applets" as C
WlSessionLockSurface {
id: root
required property WlSessionLock lock
required property LockAuth auth
color: S.Theme.base00
property real _unlockFade: 1
// Clear desktop screenshot - visible immediately
ScreencopyView {
anchors.fill: parent
captureSource: root.screen
visible: S.Modules.lock.screenshot ?? true
opacity: _unlockFade
}
// Overlay group: blur + dim + hexes, revealed per-pixel by wave position
Item {
anchors.fill: parent
opacity: _unlockFade
property bool _revealed: false
layer.enabled: !_revealed
layer.effect: ShaderEffect {
property real uPhase: _hexWave.wavePhase
property real uWidth: root.width
fragmentShader: Quickshell.shellPath("modules/reveal_mask.frag.qsb")
}
Connections {
target: _hexWave
function onWavePhaseChanged() {
if (!parent._revealed && _hexWave.wavePhase >= root.width)
parent._revealed = true;
}
}
// Blurred screenshot
ScreencopyView {
anchors.fill: parent
captureSource: root.screen
visible: S.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(S.Theme.base00.r, S.Theme.base00.g, S.Theme.base00.b, 0.4)
}
// Hex wave
C.HexWaveBackground {
id: _hexWave
anchors.fill: parent
running: root.lock.secure
opacity: 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();
}
}
// Clock - rotated, left-aligned, flies in with wave
LockClock {
id: _clockItem
anchors.left: parent.left
anchors.leftMargin: 48
anchors.top: parent.top
anchors.bottom: parent.bottom
screenHeight: root.height
wavePhase: _hexWave.wavePhase
unlockFade: root._unlockFade
}
// Center content - password and notifications
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
// Password input
LockInput {
anchors.horizontalCenter: parent.horizontalCenter
width: 280
buffer: root.auth.buffer
state: root.auth.state
}
// Error message
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: root.auth.message
color: S.Theme.base08
font.pixelSize: S.Theme.fontSize - 1
font.family: S.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
}
}
}
}
}
// Right column - widgets, fly in when wave exits screen
LockWidgets {
id: _widgetCol
anchors.right: parent.right
anchors.rightMargin: 48
anchors.verticalCenter: parent.verticalCenter
wavePhase: _hexWave.wavePhase
screenWidth: root.width
unlockFade: root._unlockFade
}
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: "_unlockFade"
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();
}
}
// Screen corners - session lock surfaces render above all layer-shell,
// so the bar's ScreenCorners aren't visible. Draw our own.
component LockCorner: Canvas {
property int corner: 0
readonly property int _r: S.Theme.screenRadius
visible: _r > 0 && S.Modules.screenCorners.enable
width: _r
height: _r
z: 999
onPaint: {
const r = _r;
const ctx = getContext("2d");
ctx.clearRect(0, 0, r, r);
ctx.fillStyle = "black";
ctx.beginPath();
switch (corner) {
case 0:
ctx.moveTo(0, 0);
ctx.lineTo(r, 0);
ctx.arc(r, r, r, -Math.PI / 2, Math.PI, true);
ctx.closePath();
break;
case 1:
ctx.moveTo(r, 0);
ctx.lineTo(0, 0);
ctx.arc(0, r, r, -Math.PI / 2, 0, false);
ctx.closePath();
break;
case 2:
ctx.moveTo(0, r);
ctx.lineTo(0, 0);
ctx.arc(r, 0, r, Math.PI, Math.PI / 2, true);
ctx.closePath();
break;
case 3:
ctx.moveTo(r, r);
ctx.lineTo(r, 0);
ctx.arc(0, 0, r, 0, Math.PI / 2, false);
ctx.closePath();
break;
}
ctx.fill();
}
}
LockCorner {
corner: 0
anchors.top: parent.top
anchors.left: parent.left
}
LockCorner {
corner: 1
anchors.top: parent.top
anchors.right: parent.right
}
LockCorner {
corner: 2
anchors.bottom: parent.bottom
anchors.left: parent.left
}
LockCorner {
corner: 3
anchors.bottom: parent.bottom
anchors.right: parent.right
}
}