lock screen: threat vignette + chromatic aberration on failed auth, heartbeat pulse at 3+ fails
This commit is contained in:
parent
94a3fdb86c
commit
8c2f362d04
3 changed files with 251 additions and 119 deletions
|
|
@ -12,6 +12,7 @@ QtObject {
|
|||
property string state: ""
|
||||
property string message: ""
|
||||
property string buffer: ""
|
||||
property int failCount: 0
|
||||
|
||||
signal unlockRequested
|
||||
signal authFailed
|
||||
|
|
@ -58,6 +59,7 @@ QtObject {
|
|||
root.message = "Wrong password";
|
||||
}
|
||||
|
||||
root.failCount++;
|
||||
root.authFailed();
|
||||
_stateReset.restart();
|
||||
}
|
||||
|
|
@ -85,6 +87,7 @@ QtObject {
|
|||
root.buffer = "";
|
||||
root.state = "";
|
||||
root.message = "";
|
||||
root.failCount = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,23 @@ WlSessionLockSurface {
|
|||
|
||||
property real _unlockFade: 1
|
||||
|
||||
// Threat level: 0.0-1.0 based on fail count (5 fails = max)
|
||||
readonly property real _threat: Math.min(1.0, root.auth.failCount / 5)
|
||||
property real _heartbeat: 0
|
||||
|
||||
// All visual content wrapped for threat shader
|
||||
Item {
|
||||
id: _visual
|
||||
anchors.fill: parent
|
||||
|
||||
layer.enabled: root._threat > 0
|
||||
layer.effect: ShaderEffect {
|
||||
property real uThreat: root._threat
|
||||
property real uPulse: root._heartbeat
|
||||
property color uColor: S.Theme.base08
|
||||
fragmentShader: Quickshell.shellPath("modules/lock_threat.frag.qsb")
|
||||
}
|
||||
|
||||
// Solid background - WlSessionLockSurface.color can be unreliable with
|
||||
// Wayland compositors, so paint an explicit opaque rect to prevent the
|
||||
// compositor fallback color (niri red) from bleeding through.
|
||||
|
|
@ -82,36 +99,6 @@ WlSessionLockSurface {
|
|||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
|
|
@ -172,6 +159,19 @@ WlSessionLockSurface {
|
|||
}
|
||||
}
|
||||
|
||||
// Fail counter - above password field
|
||||
Text {
|
||||
anchors.bottom: content.top
|
||||
anchors.bottomMargin: 24
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
visible: root.auth.failCount > 0
|
||||
text: root.auth.failCount + (root.auth.failCount === 1 ? " failed attempt" : " failed attempts")
|
||||
color: S.Theme.base08
|
||||
opacity: Math.max(0.4, root._threat)
|
||||
font.pixelSize: S.Theme.fontSize - 2
|
||||
font.family: S.Theme.fontFamily
|
||||
}
|
||||
|
||||
// Right column - widgets, fly in when wave exits screen
|
||||
LockWidgets {
|
||||
id: _widgetCol
|
||||
|
|
@ -182,6 +182,37 @@ WlSessionLockSurface {
|
|||
screenWidth: root.width
|
||||
unlockFade: root._unlockFade
|
||||
}
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible)
|
||||
|
|
@ -286,6 +317,52 @@ WlSessionLockSurface {
|
|||
}
|
||||
}
|
||||
|
||||
// Heartbeat pulse - starts at fail 3, accelerates each fail
|
||||
SequentialAnimation {
|
||||
id: _heartbeatAnim
|
||||
loops: Animation.Infinite
|
||||
running: root.auth.failCount >= 3 && root.lock.secure
|
||||
|
||||
// Systole (sharp spike)
|
||||
NumberAnimation {
|
||||
target: root
|
||||
property: "_heartbeat"
|
||||
from: 0
|
||||
to: 1
|
||||
duration: 80
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
NumberAnimation {
|
||||
target: root
|
||||
property: "_heartbeat"
|
||||
to: 0.15
|
||||
duration: 100
|
||||
easing.type: Easing.InQuad
|
||||
}
|
||||
// Diastole (smaller bump)
|
||||
NumberAnimation {
|
||||
target: root
|
||||
property: "_heartbeat"
|
||||
to: 0.5
|
||||
duration: 80
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
NumberAnimation {
|
||||
target: root
|
||||
property: "_heartbeat"
|
||||
to: 0
|
||||
duration: 120
|
||||
easing.type: Easing.InQuad
|
||||
}
|
||||
// Pause between beats - shorter as fails mount
|
||||
PauseAnimation {
|
||||
duration: Math.max(200, 800 - (root.auth.failCount - 3) * 150)
|
||||
}
|
||||
|
||||
onRunningChanged: if (!running)
|
||||
root._heartbeat = 0
|
||||
}
|
||||
|
||||
// Screen corners - session lock surfaces render above all layer-shell,
|
||||
// so the bar's ScreenCorners aren't visible. Draw our own.
|
||||
component LockCorner: Canvas {
|
||||
|
|
|
|||
52
shell/modules/lock_threat.frag
Normal file
52
shell/modules/lock_threat.frag
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
#version 440
|
||||
|
||||
layout(location = 0) in vec2 qt_TexCoord0;
|
||||
layout(location = 0) out vec4 fragColor;
|
||||
|
||||
layout(std140, binding = 0) uniform buf {
|
||||
mat4 qt_Matrix;
|
||||
float qt_Opacity;
|
||||
// 0.0 = no threat, 1.0 = max lockout
|
||||
float uThreat;
|
||||
// 0.0-1.0 heartbeat pulse (0 when inactive)
|
||||
float uPulse;
|
||||
// accent/threat color
|
||||
vec4 uColor;
|
||||
};
|
||||
|
||||
layout(binding = 1) uniform sampler2D source;
|
||||
|
||||
void main() {
|
||||
vec2 uv = qt_TexCoord0;
|
||||
float threat = clamp(uThreat, 0.0, 1.0);
|
||||
float pulse = clamp(uPulse, 0.0, 1.0);
|
||||
|
||||
// Combined intensity: base threat + heartbeat spike
|
||||
float intensity = threat + pulse * threat * 0.4;
|
||||
|
||||
// Chromatic aberration - RGB channel split increasing with threat
|
||||
float aberration = intensity * 0.008;
|
||||
float r = texture(source, uv + vec2(aberration, 0.0)).r;
|
||||
float g = texture(source, uv).g;
|
||||
float b = texture(source, uv - vec2(aberration, 0.0)).b;
|
||||
float a = texture(source, uv).a;
|
||||
vec4 tex = vec4(r, g, b, a);
|
||||
|
||||
// Vignette - radial darkening from edges, creeping inward with threat
|
||||
vec2 center = uv - 0.5;
|
||||
float dist = length(center * vec2(1.0, 0.7)); // wider horizontally
|
||||
// Inner edge shrinks as threat rises (0.7 -> 0.25)
|
||||
float vigInner = mix(0.7, 0.25, intensity);
|
||||
// Outer edge also shrinks (1.0 -> 0.6)
|
||||
float vigOuter = mix(1.0, 0.6, intensity);
|
||||
float vig = smoothstep(vigInner, vigOuter, dist);
|
||||
|
||||
// Red tint on the vignette area
|
||||
vec3 threatColor = uColor.rgb;
|
||||
vec3 tinted = mix(tex.rgb, threatColor * 0.15, vig * intensity);
|
||||
|
||||
// Darken edges
|
||||
tinted *= 1.0 - vig * intensity * 0.7;
|
||||
|
||||
fragColor = vec4(tinted, tex.a) * qt_Opacity;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue