aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-09-17 14:26:20 +0200
committerDanilo M. <danix@danix.xyz>2026-09-17 14:26:20 +0200
commitcdcf62416feffcb81cb07fdfe2e2dc0320490829 (patch)
tree2f0f299aeecb574043dd188c93d7512a0d9b07e2
parent25c82713f500c9c254948e05aa278ad7d125d751 (diff)
downloadsddm-theme-udt-cdcf62416feffcb81cb07fdfe2e2dc0320490829.tar.gz
sddm-theme-udt-cdcf62416feffcb81cb07fdfe2e2dc0320490829.zip
feat: add the staged username and password credential slot
-rw-r--r--theme/components/Credentials.qml180
1 files changed, 180 insertions, 0 deletions
diff --git a/theme/components/Credentials.qml b/theme/components/Credentials.qml
new file mode 100644
index 0000000..c2aba29
--- /dev/null
+++ b/theme/components/Credentials.qml
@@ -0,0 +1,180 @@
+// SPDX-License-Identifier: GPL-2.0-only
+// Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+//
+// One credential slot, two stages. The username shows first; confirming it
+// slides the username out and the password in. Confirming the password emits
+// submitted(). A back chevron, or Esc, returns to the username.
+
+import QtQuick
+import QtQuick.Controls.Basic
+
+Item {
+ id: creds
+
+ // Emitted when the password stage is confirmed, and on input so the panel
+ // can restart its idle timer.
+ signal submitted()
+ signal wake()
+
+ property string stage: "user"
+ readonly property string username: usernameField.text
+ readonly property string password: passwordField.text
+ readonly property bool passwordStage: stage === "pass"
+
+ implicitHeight: 44
+
+ function confirm() {
+ if (!creds.passwordStage) {
+ if (usernameField.text.length > 0)
+ creds.stage = "pass"
+ } else {
+ creds.submitted()
+ }
+ }
+
+ function clearPassword() {
+ passwordField.text = ""
+ }
+
+ function focusStage() {
+ if (creds.passwordStage)
+ passwordField.forceActiveFocus()
+ else
+ usernameField.forceActiveFocus()
+ }
+
+ onStageChanged: focusStage()
+ Component.onCompleted: focusStage()
+
+ // Back chevron, password stage only. The width animates so the slot grows
+ // and shrinks with it.
+ Item {
+ id: back
+ anchors.left: parent.left
+ anchors.verticalCenter: parent.verticalCenter
+ width: creds.passwordStage ? 44 : 0
+ height: 44
+ clip: true
+ visible: width > 0
+
+ Rectangle {
+ anchors.fill: parent
+ radius: 6
+ color: config.stringValue("surface0")
+ opacity: backMouse.containsMouse ? 1 : 0
+ }
+
+ Text {
+ anchors.centerIn: parent
+ text: "\u2190"
+ color: config.stringValue("subtext0")
+ font.family: "Noto Sans"
+ font.pixelSize: 22
+ }
+
+ MouseArea {
+ id: backMouse
+ anchors.fill: parent
+ hoverEnabled: true
+ onClicked: {
+ creds.clearPassword()
+ creds.stage = "user"
+ }
+ }
+ }
+
+ Button {
+ id: confirmButton
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ width: 44
+ height: 44
+ padding: 0
+ background: Rectangle {
+ radius: 6
+ color: config.stringValue("accent")
+ }
+ contentItem: Text {
+ text: "\u2192"
+ color: config.stringValue("base")
+ font.family: "Noto Sans"
+ font.pixelSize: 22
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ }
+ onClicked: creds.confirm()
+ }
+
+ // The sliding slot. Both fields live here, clipped, and slide across.
+ Item {
+ id: slot
+ anchors.left: back.right
+ anchors.leftMargin: 8
+ anchors.right: confirmButton.left
+ anchors.rightMargin: 8
+ anchors.verticalCenter: parent.verticalCenter
+ height: 44
+ clip: true
+
+ TextField {
+ id: usernameField
+ width: slot.width
+ height: slot.height
+ text: userModel.lastUser !== undefined ? userModel.lastUser : ""
+ placeholderText: "Username"
+ font.family: "Noto Sans"
+ color: config.stringValue("text")
+ palette.base: config.stringValue("surface0")
+ palette.text: config.stringValue("text")
+ palette.highlight: config.stringValue("accent")
+ palette.highlightedText: config.stringValue("base")
+ background: Rectangle {
+ radius: 6
+ color: config.stringValue("surface0")
+ }
+ x: creds.passwordStage ? -slot.width : 0
+ opacity: creds.passwordStage ? 0 : 1
+ Behavior on x { NumberAnimation { duration: 220; easing.type: Easing.OutCubic } }
+ Behavior on opacity { NumberAnimation { duration: 220 } }
+ onAccepted: creds.confirm()
+ onTextChanged: creds.wake()
+ Keys.onPressed: event => {
+ creds.wake()
+ event.accepted = false
+ }
+ }
+
+ TextField {
+ id: passwordField
+ width: slot.width
+ height: slot.height
+ echoMode: TextInput.Password
+ placeholderText: "Password"
+ font.family: "Noto Sans"
+ color: config.stringValue("text")
+ palette.base: config.stringValue("surface0")
+ palette.text: config.stringValue("text")
+ palette.highlight: config.stringValue("accent")
+ palette.highlightedText: config.stringValue("base")
+ background: Rectangle {
+ radius: 6
+ color: config.stringValue("surface0")
+ }
+ x: creds.passwordStage ? 0 : slot.width
+ opacity: creds.passwordStage ? 1 : 0
+ Behavior on x { NumberAnimation { duration: 220; easing.type: Easing.OutCubic } }
+ Behavior on opacity { NumberAnimation { duration: 220 } }
+ onAccepted: creds.confirm()
+ Keys.onPressed: event => {
+ creds.wake()
+ if (event.key === Qt.Key_Escape) {
+ creds.clearPassword()
+ creds.stage = "user"
+ event.accepted = true
+ } else {
+ event.accepted = false
+ }
+ }
+ }
+ }
+}