Redesign the lock screen
This commit is contained in:
1 parent
8b0ce5baf3
commit
4c60929aff
2 files changed
+198
-57
No files matched your search
+6
-3
@@ -69,9 +69,12 @@ 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 carries `branding/screensaver.txt` and an AGS-style password
|
- The lock screen shows a large clock and the date over the blurred, dimmed
|
||||||
field: square corners, a 2px border on the blue slot at half alpha going to
|
wallpaper, then an avatar with the user's initial and name, and a rounded
|
||||||
solid accent once typing starts, and urgent on a failed attempt.
|
password field with a lock icon and, when enrolled, a fingerprint icon. The
|
||||||
|
border goes to the blue slot while focused or typing and to urgent on a
|
||||||
|
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).
|
||||||
|
|
||||||
|
|||||||
+192
-54
@@ -1,5 +1,6 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Effects
|
import QtQuick.Effects
|
||||||
|
import Quickshell
|
||||||
import qs.Commons
|
import qs.Commons
|
||||||
import qs.Ui
|
import qs.Ui
|
||||||
|
|
||||||
@@ -18,45 +19,56 @@ 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: 381
|
readonly property int fieldWidth: 360
|
||||||
readonly property int fieldHeight: 67
|
readonly property int fieldHeight: 52
|
||||||
readonly property int outlineThickness: 2
|
readonly property int outlineThickness: 2
|
||||||
readonly property int fieldRadius: 0
|
readonly property int fieldRadius: fieldHeight / 2
|
||||||
readonly property int fieldFontSize: Math.round(Style.font.heading * 1.125)
|
readonly property int fieldSideInset: 20
|
||||||
readonly property int passwordDotFontSize: Math.round(Style.font.heading * 1.33)
|
readonly property int fieldFontSize: Style.font.heading
|
||||||
|
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)
|
||||||
// Space to keep clear on each side of the field for the fingerprint icon
|
readonly property int clockFontSize: Math.round(Style.font.display * 4)
|
||||||
// (icon width plus a gap) so the centered dots never run under it.
|
readonly property int dateFontSize: Math.round(Style.font.title * 1.4)
|
||||||
readonly property real fingerprintReserve: fingerprintConfigured ? Math.round(fingerprintIcon.implicitWidth + 12) : 0
|
readonly property int avatarSize: 72
|
||||||
// Shrink the dots to fit once the password outgrows the field, so every
|
readonly property real iconReserve: Math.round(lockIcon.implicitWidth + 12)
|
||||||
// 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 brandingGap: Style.space(48)
|
readonly property int brandingMaxWidth: 700
|
||||||
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 bool inputActive: passwordText.length > 0 || authenticatingPassword
|
readonly property bool inputActive: passwordText.length > 0 || authenticatingPassword
|
||||||
readonly property color inputRestingBorder: paletteColor4.length > 0 ? paletteColor4 : Color.accent
|
readonly property color accentColor: paletteColor4.length > 0 ? paletteColor4 : Color.accent
|
||||||
readonly property color inputBorderColor: errorState
|
readonly property color inputBorderColor: errorState
|
||||||
? Color.urgent
|
? Color.urgent
|
||||||
: (inputActive ? Color.accent : Util.alpha(root.inputRestingBorder, 0.5))
|
: (inputActive || passwordInput.activeFocus ? root.accentColor : Util.alpha(Color.lock.text, 0.25))
|
||||||
readonly property color inputBackground: Util.alpha(Color.background, 0.6)
|
readonly property color inputBackground: Util.alpha(Color.background, 0.55)
|
||||||
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("/")
|
||||||
@@ -79,6 +91,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onPasswordTextChanged: syncPasswordText()
|
onPasswordTextChanged: syncPasswordText()
|
||||||
|
onFailureMessageChanged: if (failureMessage.length > 0) shakeAnimation.restart()
|
||||||
onInputEnabledChanged: {
|
onInputEnabledChanged: {
|
||||||
if (inputEnabled) Qt.callLater(forcePasswordFocus)
|
if (inputEnabled) Qt.callLater(forcePasswordFocus)
|
||||||
}
|
}
|
||||||
@@ -87,8 +100,11 @@ Item {
|
|||||||
if (inputEnabled) Qt.callLater(forcePasswordFocus)
|
if (inputEnabled) Qt.callLater(forcePasswordFocus)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Measures the masked password at full size; passwordDotScale compares this
|
SystemClock {
|
||||||
// against the field width to decide how far the dots must shrink to fit.
|
id: clock
|
||||||
|
precision: SystemClock.Minutes
|
||||||
|
}
|
||||||
|
|
||||||
TextMetrics {
|
TextMetrics {
|
||||||
id: dotMetrics
|
id: dotMetrics
|
||||||
font.family: Style.font.family
|
font.family: Style.font.family
|
||||||
@@ -97,6 +113,15 @@ 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
|
||||||
@@ -123,6 +148,15 @@ 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
|
||||||
@@ -130,44 +164,115 @@ Item {
|
|||||||
onPositionChanged: root.wakeRequested()
|
onPositionChanged: root.wakeRequested()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: content
|
||||||
|
anchors.centerIn: parent
|
||||||
|
anchors.verticalCenterOffset: -Math.round(parent.height * 0.04)
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: branding
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
anchors.bottom: inputField.top
|
text: Qt.formatDateTime(clock.date, "HH:mm")
|
||||||
anchors.bottomMargin: root.brandingGap
|
|
||||||
width: Math.min(parent.width * 0.86, root.brandingMaxWidth)
|
|
||||||
height: Math.max(0, inputField.y - root.brandingGap * 2)
|
|
||||||
visible: root.brandingText.length > 0 && height > 0
|
|
||||||
text: root.brandingText
|
|
||||||
textFormat: Text.PlainText
|
textFormat: Text.PlainText
|
||||||
color: Color.lock.text
|
color: Color.lock.text
|
||||||
font.family: Style.font.family
|
font.family: Style.font.family
|
||||||
font.pixelSize: root.brandingMaxFontSize
|
font.pixelSize: root.clockFontSize
|
||||||
minimumPixelSize: 4
|
font.weight: Font.Light
|
||||||
fontSizeMode: Text.Fit
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
verticalAlignment: Text.AlignBottom
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
text: Qt.formatDateTime(clock.date, "dddd, d MMMM")
|
||||||
|
textFormat: Text.PlainText
|
||||||
|
color: Util.alpha(Color.lock.text, 0.8)
|
||||||
|
font.family: Style.font.family
|
||||||
|
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 {
|
BorderSurface {
|
||||||
id: inputField
|
id: inputField
|
||||||
width: root.fieldWidth
|
width: parent.width
|
||||||
height: root.fieldHeight
|
height: parent.height
|
||||||
anchors.centerIn: parent
|
x: root.shakeOffset
|
||||||
color: root.inputBackground
|
color: root.inputBackground
|
||||||
borderSpec: root.inputBorderSpec
|
borderSpec: root.inputBorderSpec
|
||||||
radius: root.fieldRadius
|
radius: root.fieldRadius
|
||||||
clip: true
|
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 {
|
TextInput {
|
||||||
id: passwordInput
|
id: passwordInput
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.topMargin: inputField.borderTop
|
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.bottomMargin: inputField.borderBottom
|
||||||
anchors.leftMargin: inputField.borderLeft + 18 + root.fingerprintReserve
|
anchors.leftMargin: inputField.borderLeft + root.fieldSideInset + root.iconReserve
|
||||||
|
anchors.rightMargin: inputField.borderRight + root.fieldSideInset + root.iconReserve
|
||||||
verticalAlignment: TextInput.AlignVCenter
|
verticalAlignment: TextInput.AlignVCenter
|
||||||
horizontalAlignment: TextInput.AlignHCenter
|
horizontalAlignment: TextInput.AlignHCenter
|
||||||
activeFocusOnPress: true
|
activeFocusOnPress: true
|
||||||
@@ -175,7 +280,7 @@ Item {
|
|||||||
enabled: root.inputEnabled && !root.authenticatingPassword
|
enabled: root.inputEnabled && !root.authenticatingPassword
|
||||||
readOnly: root.authenticatingPassword
|
readOnly: root.authenticatingPassword
|
||||||
echoMode: TextInput.Password
|
echoMode: TextInput.Password
|
||||||
passwordCharacter: "\u25CF"
|
passwordCharacter: "●"
|
||||||
passwordMaskDelay: 0
|
passwordMaskDelay: 0
|
||||||
color: Color.lock.text
|
color: Color.lock.text
|
||||||
selectionColor: Color.lock.selection
|
selectionColor: Color.lock.selection
|
||||||
@@ -192,9 +297,7 @@ Item {
|
|||||||
|
|
||||||
onTextChanged: {
|
onTextChanged: {
|
||||||
if (!root.syncingPasswordText) root.passwordTextEdited(text)
|
if (!root.syncingPasswordText) root.passwordTextEdited(text)
|
||||||
if (text.length > 0) {
|
if (text.length > 0) root.wakeRequested()
|
||||||
root.wakeRequested()
|
|
||||||
}
|
|
||||||
if (text.length > 0 && root.failureMessage.length > 0) root.clearFailureRequested()
|
if (text.length > 0 && root.failureMessage.length > 0) root.clearFailureRequested()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,36 +317,71 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
textFormat: Text.PlainText
|
|
||||||
anchors.fill: passwordInput
|
anchors.fill: passwordInput
|
||||||
text: root.authenticatingPassword ? "Checking…" : (root.failureMessage.length > 0 ? root.failureMessage : root.placeholderText)
|
|
||||||
visible: passwordInput.text.length === 0
|
visible: passwordInput.text.length === 0
|
||||||
color: root.authenticatingPassword ? Color.lock.text : (root.failureMessage.length > 0 ? Color.lock.textError : Color.lock.placeholder)
|
text: root.authenticatingPassword ? "Checking…" : root.placeholderText
|
||||||
|
textFormat: Text.PlainText
|
||||||
|
color: Color.lock.placeholder
|
||||||
font.family: Style.font.family
|
font.family: Style.font.family
|
||||||
font.pixelSize: root.fieldFontSize
|
font.pixelSize: root.fieldFontSize
|
||||||
font.italic: !root.authenticatingPassword && root.failureMessage.length > 0
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
elide: Text.ElideRight
|
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 {
|
Text {
|
||||||
id: fingerprintIcon
|
id: fingerprintIcon
|
||||||
objectName: "fingerprintIndicator"
|
objectName: "fingerprintIndicator"
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.rightMargin: inputField.borderRight + 18
|
anchors.rightMargin: inputField.borderRight + root.fieldSideInset
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
visible: root.fingerprintConfigured
|
visible: root.fingerprintConfigured
|
||||||
text: ""
|
text: ""
|
||||||
|
textFormat: Text.PlainText
|
||||||
color: Color.lock.placeholder
|
color: Color.lock.placeholder
|
||||||
font.family: Style.font.family
|
font.family: Style.font.family
|
||||||
font.pixelSize: Math.round(root.fieldFontSize * 1.1)
|
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
|
horizontalAlignment: Text.AlignHCenter
|
||||||
verticalAlignment: Text.AlignVCenter
|
color: root.errorState ? Color.lock.textError : Util.alpha(Color.lock.text, 0.6)
|
||||||
|
font.family: Style.font.family
|
||||||
|
font.pixelSize: Style.font.body
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: branding
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.bottomMargin: Style.space(40)
|
||||||
|
width: Math.min(parent.width * 0.6, root.brandingMaxWidth)
|
||||||
|
height: Math.max(0, Math.min(parent.height * 0.18,
|
||||||
|
parent.height - (content.y + content.height) - Style.space(80)))
|
||||||
|
visible: root.brandingText.length > 0 && height > Style.space(24)
|
||||||
|
opacity: 0.45
|
||||||
|
text: root.brandingText
|
||||||
|
textFormat: Text.PlainText
|
||||||
|
color: Color.lock.text
|
||||||
|
font.family: Style.font.family
|
||||||
|
font.pixelSize: root.brandingMaxFontSize
|
||||||
|
minimumPixelSize: 4
|
||||||
|
fontSizeMode: Text.Fit
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
verticalAlignment: Text.AlignBottom
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user