Return to the simple lock screen with the time and date

This commit is contained in:
SirBlob committed 2026-09-24 03:28:32 +00:00
1 parent ad46fe4a53
commit e9b7e594c3
3 files changed
+134 -241

No files matched your search

File renamed without changes.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 79 KiB

+4 -6
View File
@@ -71,12 +71,10 @@ changes beyond renaming:
- The `Dictation` bar indicator removed with voice typing. - The `Dictation` bar indicator removed with voice typing.
- Persistent workspaces 1-9 instead of 1-5. - Persistent workspaces 1-9 instead of 1-5.
- Menu cards 440 wide instead of 300, with the Blob icon on the bar button. - Menu cards 440 wide instead of 300, with the Blob icon on the bar button.
- The lock screen shows a large clock and the date over the blurred, dimmed - The lock screen carries `branding/screensaver.txt`, the time and date above
wallpaper, then an avatar with the user's initial and name, and a rounded the password field, and an AGS-style password field: square corners, a 2px
password field with a lock icon and, when enrolled, a fingerprint icon. The border on the blue slot at half alpha going to solid accent once typing
border goes to the blue slot while focused or typing and to urgent on a starts, and urgent on a failed attempt.
failed attempt, when the field shakes and the line under it shows the error
and attempt count. `branding/screensaver.txt` sits faded at the bottom.
- `quick-settings`, `sysmon`, and `notification-center` are new, ported from the - `quick-settings`, `sysmon`, and `notification-center` are new, ported from the
GTK widgets. See [widgets.md](widgets.md). GTK widgets. See [widgets.md](widgets.md).
+130 -235
View File
@@ -19,56 +19,47 @@ Item {
property bool loadBackground: true property bool loadBackground: true
property string passwordText: "" property string passwordText: ""
property bool syncingPasswordText: false property bool syncingPasswordText: false
property string userName: Quickshell.env("USER") || Quickshell.env("LOGNAME") || ""
readonly property string placeholderText: "Enter password" readonly property string placeholderText: "Enter Password"
readonly property int fieldWidth: 360 readonly property int fieldWidth: 381
readonly property int fieldHeight: 52 readonly property int fieldHeight: 67
readonly property int outlineThickness: 2 readonly property int outlineThickness: 2
readonly property int fieldRadius: fieldHeight / 2 readonly property int fieldRadius: 0
readonly property int fieldSideInset: 20 readonly property int fieldFontSize: Math.round(Style.font.heading * 1.125)
readonly property int fieldFontSize: Style.font.heading readonly property int passwordDotFontSize: Math.round(Style.font.heading * 1.33)
readonly property int passwordDotFontSize: Math.round(Style.font.heading * 1.2)
readonly property int passwordDotLetterSpacing: Math.round(Style.font.heading * 0.19) readonly property int passwordDotLetterSpacing: Math.round(Style.font.heading * 0.19)
readonly property int clockFontSize: Math.round(Style.font.display * 4) // Space to keep clear on each side of the field for the fingerprint icon
readonly property int dateFontSize: Math.round(Style.font.title * 1.4) // (icon width plus a gap) so the centered dots never run under it.
readonly property int avatarSize: 72 readonly property real fingerprintReserve: fingerprintConfigured ? Math.round(fingerprintIcon.implicitWidth + 12) : 0
readonly property real iconReserve: Math.round(lockIcon.implicitWidth + 12) // Shrink the dots to fit once the password outgrows the field, so every
// keystroke stays visible — otherwise long passwords clip with no feedback.
readonly property real passwordDotScale: dotMetrics.advanceWidth > 0 readonly property real passwordDotScale: dotMetrics.advanceWidth > 0
? Math.min(1, (passwordInput.width - 4) / dotMetrics.advanceWidth) ? Math.min(1, (passwordInput.width - 4) / dotMetrics.advanceWidth)
: 1 : 1
readonly property bool showPasswordCursor: inputEnabled && !authenticatingPassword && failureMessage.length === 0 readonly property bool showPasswordCursor: inputEnabled && !authenticatingPassword && failureMessage.length === 0
readonly property bool errorState: failureMessage.length > 0 readonly property bool errorState: failureMessage.length > 0
readonly property int brandingMaxWidth: 700 readonly property int brandingGap: Style.space(48)
readonly property int brandingMaxWidth: 1100
readonly property int brandingMaxFontSize: Math.round(Style.font.heading * 1.5) readonly property int brandingMaxFontSize: Math.round(Style.font.heading * 1.5)
readonly property int clockFontSize: Math.round(Style.font.display * 3)
readonly property int dateFontSize: Math.round(Style.font.heading * 1.125)
readonly property bool inputActive: passwordText.length > 0 || authenticatingPassword readonly property bool inputActive: passwordText.length > 0 || authenticatingPassword
readonly property color accentColor: paletteColor4.length > 0 ? paletteColor4 : Color.accent readonly property color inputRestingBorder: paletteColor4.length > 0 ? paletteColor4 : Color.accent
readonly property color inputBorderColor: errorState readonly property color inputBorderColor: errorState
? Color.urgent ? Color.urgent
: (inputActive || passwordInput.activeFocus ? root.accentColor : Util.alpha(Color.lock.text, 0.25)) : (inputActive ? Color.accent : Util.alpha(root.inputRestingBorder, 0.5))
readonly property color inputBackground: Util.alpha(Color.background, 0.55) readonly property color inputBackground: Util.alpha(Color.background, 0.6)
readonly property var inputBorderSpec: Border.flat(root.inputBorderColor, root.outlineThickness) readonly property var inputBorderSpec: Border.flat(root.inputBorderColor, root.outlineThickness)
readonly property string statusText: {
if (root.authenticatingPassword) return "Checking…"
if (root.errorState) {
return root.failedAttempts > 1
? root.failureMessage + " (" + root.failedAttempts + " attempts)"
: root.failureMessage
}
return root.fingerprintConfigured
? "Type your password or touch the fingerprint sensor"
: "Type your password and press Enter"
}
property real shakeOffset: 0
signal submitPassword(string password) signal submitPassword(string password)
signal passwordTextEdited(string password) signal passwordTextEdited(string password)
signal clearFailureRequested() signal clearFailureRequested()
signal wakeRequested() signal wakeRequested()
// Cache-busts the lock background by appending `?v=`. Adding a query
// string keeps Image's loader happy while forcing it to reload when the
// user picks a new background mid-session.
function fileUrl(path) { function fileUrl(path) {
if (!path) return "" if (!path) return ""
var encoded = String(path).split("/").map(encodeURIComponent).join("/") var encoded = String(path).split("/").map(encodeURIComponent).join("/")
@@ -91,7 +82,6 @@ Item {
} }
onPasswordTextChanged: syncPasswordText() onPasswordTextChanged: syncPasswordText()
onFailureMessageChanged: if (failureMessage.length > 0) shakeAnimation.restart()
onInputEnabledChanged: { onInputEnabledChanged: {
if (inputEnabled) Qt.callLater(forcePasswordFocus) if (inputEnabled) Qt.callLater(forcePasswordFocus)
} }
@@ -100,6 +90,8 @@ Item {
if (inputEnabled) Qt.callLater(forcePasswordFocus) if (inputEnabled) Qt.callLater(forcePasswordFocus)
} }
// Measures the masked password at full size; passwordDotScale compares this
// against the field width to decide how far the dots must shrink to fit.
SystemClock { SystemClock {
id: clock id: clock
precision: SystemClock.Minutes precision: SystemClock.Minutes
@@ -113,15 +105,6 @@ Item {
text: "●".repeat(passwordInput.text.length) text: "●".repeat(passwordInput.text.length)
} }
SequentialAnimation {
id: shakeAnimation
NumberAnimation { target: root; property: "shakeOffset"; to: -14; duration: 50 }
NumberAnimation { target: root; property: "shakeOffset"; to: 12; duration: 70 }
NumberAnimation { target: root; property: "shakeOffset"; to: -8; duration: 60 }
NumberAnimation { target: root; property: "shakeOffset"; to: 5; duration: 50 }
NumberAnimation { target: root; property: "shakeOffset"; to: 0; duration: 40 }
}
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
color: Color.background color: Color.background
@@ -148,15 +131,6 @@ Item {
contrast: -0.08 contrast: -0.08
} }
Rectangle {
anchors.fill: parent
gradient: Gradient {
GradientStop { position: 0.0; color: Util.alpha(Color.background, 0.2) }
GradientStop { position: 0.5; color: Util.alpha(Color.background, 0.35) }
GradientStop { position: 1.0; color: Util.alpha(Color.background, 0.7) }
}
}
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
@@ -165,10 +139,10 @@ Item {
} }
Column { Column {
id: content id: clockColumn
anchors.centerIn: parent anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenterOffset: -Math.round(parent.height * 0.04) anchors.bottom: inputField.top
spacing: 0 anchors.bottomMargin: root.brandingGap
Text { Text {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
@@ -177,202 +151,26 @@ Item {
color: Color.lock.text color: Color.lock.text
font.family: Style.font.family font.family: Style.font.family
font.pixelSize: root.clockFontSize font.pixelSize: root.clockFontSize
font.weight: Font.Light
} }
Text { Text {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
text: Qt.formatDateTime(clock.date, "dddd, d MMMM") text: Qt.formatDateTime(clock.date, "dddd, d MMMM")
textFormat: Text.PlainText textFormat: Text.PlainText
color: Util.alpha(Color.lock.text, 0.8) color: Color.lock.placeholder
font.family: Style.font.family font.family: Style.font.family
font.pixelSize: root.dateFontSize font.pixelSize: root.dateFontSize
} }
Item {
width: 1
height: Style.space(56)
}
Rectangle {
anchors.horizontalCenter: parent.horizontalCenter
width: root.avatarSize
height: root.avatarSize
radius: width / 2
color: Util.alpha(root.accentColor, 0.22)
border.width: root.outlineThickness
border.color: root.accentColor
Text {
anchors.centerIn: parent
text: root.userName.length > 0 ? root.userName.charAt(0).toUpperCase() : ""
textFormat: Text.PlainText
color: Color.lock.text
font.family: Style.font.family
font.pixelSize: Math.round(root.avatarSize * 0.42)
font.bold: true
}
}
Item {
width: 1
height: Style.space(12)
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: root.userName
visible: root.userName.length > 0
textFormat: Text.PlainText
color: Color.lock.text
font.family: Style.font.family
font.pixelSize: Style.font.title
font.bold: true
}
Item {
width: 1
height: Style.space(20)
}
Item {
anchors.horizontalCenter: parent.horizontalCenter
width: root.fieldWidth
height: root.fieldHeight
BorderSurface {
id: inputField
width: parent.width
height: parent.height
x: root.shakeOffset
color: root.inputBackground
borderSpec: root.inputBorderSpec
radius: root.fieldRadius
clip: true
Behavior on color {
ColorAnimation { duration: 160 }
}
Text {
id: lockIcon
anchors.left: parent.left
anchors.leftMargin: inputField.borderLeft + root.fieldSideInset
anchors.verticalCenter: parent.verticalCenter
text: root.errorState ? "" : ""
textFormat: Text.PlainText
color: root.errorState ? Color.lock.textError : Color.lock.placeholder
font.family: Style.font.family
font.pixelSize: Math.round(root.fieldFontSize * 1.05)
}
TextInput {
id: passwordInput
anchors.fill: parent
anchors.topMargin: inputField.borderTop
anchors.bottomMargin: inputField.borderBottom
anchors.leftMargin: inputField.borderLeft + root.fieldSideInset + root.iconReserve
anchors.rightMargin: inputField.borderRight + root.fieldSideInset + root.iconReserve
verticalAlignment: TextInput.AlignVCenter
horizontalAlignment: TextInput.AlignHCenter
activeFocusOnPress: true
clip: true
enabled: root.inputEnabled && !root.authenticatingPassword
readOnly: root.authenticatingPassword
echoMode: TextInput.Password
passwordCharacter: "●"
passwordMaskDelay: 0
color: Color.lock.text
selectionColor: Color.lock.selection
selectedTextColor: Color.lock.text
font.family: Style.font.family
font.pixelSize: text.length > 0 ? Math.max(1, Math.floor(root.passwordDotFontSize * root.passwordDotScale)) : root.fieldFontSize
font.letterSpacing: text.length > 0 ? root.passwordDotLetterSpacing * root.passwordDotScale : 0
cursorVisible: activeFocus && root.showPasswordCursor && text.length > 0
cursorDelegate: Rectangle {
width: 2
color: Color.lock.text
visible: passwordInput.cursorVisible
}
onTextChanged: {
if (!root.syncingPasswordText) root.passwordTextEdited(text)
if (text.length > 0) root.wakeRequested()
if (text.length > 0 && root.failureMessage.length > 0) root.clearFailureRequested()
}
onAccepted: {
var submitted = root.passwordText
root.passwordTextEdited("")
if (submitted.length > 0) root.submitPassword(submitted)
}
Keys.onPressed: function(event) {
root.wakeRequested()
if (event.key === Qt.Key_Escape || (event.modifiers & Qt.ControlModifier && event.key === Qt.Key_U)) {
root.passwordTextEdited("")
event.accepted = true
}
}
}
Text {
anchors.fill: passwordInput
visible: passwordInput.text.length === 0
text: root.authenticatingPassword ? "Checking…" : root.placeholderText
textFormat: Text.PlainText
color: Color.lock.placeholder
font.family: Style.font.family
font.pixelSize: root.fieldFontSize
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
Text {
id: fingerprintIcon
objectName: "fingerprintIndicator"
anchors.right: parent.right
anchors.rightMargin: inputField.borderRight + root.fieldSideInset
anchors.verticalCenter: parent.verticalCenter
visible: root.fingerprintConfigured
text: "󰈷"
textFormat: Text.PlainText
color: Color.lock.placeholder
font.family: Style.font.family
font.pixelSize: Math.round(root.fieldFontSize * 1.2)
}
}
}
Item {
width: 1
height: Style.space(14)
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
width: root.fieldWidth + Style.space(80)
text: root.statusText
textFormat: Text.PlainText
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
color: root.errorState ? Color.lock.textError : Util.alpha(Color.lock.text, 0.6)
font.family: Style.font.family
font.pixelSize: Style.font.body
}
} }
Text { Text {
id: branding id: branding
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom anchors.bottom: clockColumn.top
anchors.bottomMargin: Style.space(40) anchors.bottomMargin: root.brandingGap
width: Math.min(parent.width * 0.6, root.brandingMaxWidth) width: Math.min(parent.width * 0.86, root.brandingMaxWidth)
height: Math.max(0, Math.min(parent.height * 0.18, height: Math.max(0, clockColumn.y - root.brandingGap * 2)
parent.height - (content.y + content.height) - Style.space(80))) visible: root.brandingText.length > 0 && height > 0
visible: root.brandingText.length > 0 && height > Style.space(24)
opacity: 0.45
text: root.brandingText text: root.brandingText
textFormat: Text.PlainText textFormat: Text.PlainText
color: Color.lock.text color: Color.lock.text
@@ -383,5 +181,102 @@ Item {
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignBottom verticalAlignment: Text.AlignBottom
} }
BorderSurface {
id: inputField
width: root.fieldWidth
height: root.fieldHeight
anchors.centerIn: parent
color: root.inputBackground
borderSpec: root.inputBorderSpec
radius: root.fieldRadius
clip: true
TextInput {
id: passwordInput
anchors.fill: parent
anchors.topMargin: inputField.borderTop
// Reserve the fingerprint icon's width on both sides so the centered
// dots stay symmetric and never slide under the icon as they grow.
anchors.rightMargin: inputField.borderRight + 18 + root.fingerprintReserve
anchors.bottomMargin: inputField.borderBottom
anchors.leftMargin: inputField.borderLeft + 18 + root.fingerprintReserve
verticalAlignment: TextInput.AlignVCenter
horizontalAlignment: TextInput.AlignHCenter
activeFocusOnPress: true
clip: true
enabled: root.inputEnabled && !root.authenticatingPassword
readOnly: root.authenticatingPassword
echoMode: TextInput.Password
passwordCharacter: "\u25CF"
passwordMaskDelay: 0
color: Color.lock.text
selectionColor: Color.lock.selection
selectedTextColor: Color.lock.text
font.family: Style.font.family
font.pixelSize: text.length > 0 ? Math.max(1, Math.floor(root.passwordDotFontSize * root.passwordDotScale)) : root.fieldFontSize
font.letterSpacing: text.length > 0 ? root.passwordDotLetterSpacing * root.passwordDotScale : 0
cursorVisible: activeFocus && root.showPasswordCursor && text.length > 0
cursorDelegate: Rectangle {
width: 2
color: Color.lock.text
visible: passwordInput.cursorVisible
}
onTextChanged: {
if (!root.syncingPasswordText) root.passwordTextEdited(text)
if (text.length > 0) {
root.wakeRequested()
}
if (text.length > 0 && root.failureMessage.length > 0) root.clearFailureRequested()
}
onAccepted: {
var submitted = root.passwordText
root.passwordTextEdited("")
if (submitted.length > 0) root.submitPassword(submitted)
}
Keys.onPressed: function(event) {
root.wakeRequested()
if (event.key === Qt.Key_Escape || (event.modifiers & Qt.ControlModifier && event.key === Qt.Key_U)) {
root.passwordTextEdited("")
event.accepted = true
}
}
}
Text {
textFormat: Text.PlainText
anchors.fill: passwordInput
text: root.authenticatingPassword ? "Checking…" : (root.failureMessage.length > 0 ? root.failureMessage : root.placeholderText)
visible: passwordInput.text.length === 0
color: root.authenticatingPassword ? Color.lock.text : (root.failureMessage.length > 0 ? Color.lock.textError : Color.lock.placeholder)
font.family: Style.font.family
font.pixelSize: root.fieldFontSize
font.italic: !root.authenticatingPassword && root.failureMessage.length > 0
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
// Fingerprint hint pinned inside the field's right edge when a sensor is
// enrolled, so the user knows they can touch to unlock instead of typing.
// Matches hyprlock, which draws its fingerprint icon in the same spot.
Text {
id: fingerprintIcon
objectName: "fingerprintIndicator"
anchors.right: parent.right
anchors.rightMargin: inputField.borderRight + 18
anchors.verticalCenter: parent.verticalCenter
visible: root.fingerprintConfigured
text: "󰈷"
color: Color.lock.placeholder
font.family: Style.font.family
font.pixelSize: Math.round(root.fieldFontSize * 1.1)
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
}
} }
} }