// SPDX-License-Identifier: GPL-2.0-only // Copyright (C) 2026 Danilo M. // // The login card: avatar, username, password, session choice, feedback and the // power bar. Reads the palette straight from `config`; SDDM merges theme.conf // and the live theme.conf.user into it. import QtQuick import QtQuick.Controls.Basic import QtQuick.Effects Item { id: card // The wallpaper image to frost behind the card. Null falls back to a flat // translucent surface. property Item backdropSource: null readonly property int radius: 10 signal wake() // Shake on a failed login without touching the card's own x binding, which // Main owns: animating x directly would destroy that binding. property real shakeOffset: 0 implicitWidth: 380 implicitHeight: column.implicitHeight + 2 * 28 // Flat translucent surface. Task 4 layers the blur behind it. Rectangle { anchors.fill: parent radius: card.radius color: config.stringValue("mantle") opacity: 0.82 } Rectangle { anchors.fill: parent radius: card.radius color: "transparent" border.width: 2 border.color: config.stringValue("accent") } // Avatar lookup. userModel is a list model; Repeater delegates expose its // roles by name. The matching face is copied out on demand because the // username can change. property string avatarPath: "" readonly property string initials: usernameField.text.length > 0 ? usernameField.text.charAt(0).toUpperCase() : "?" Repeater { id: faces model: userModel delegate: Item { readonly property string faceName: name readonly property string faceIcon: icon visible: false } } function refreshAvatar() { for (var i = 0; i < faces.count; ++i) { var it = faces.itemAt(i) if (it && it.faceName === usernameField.text) { avatarPath = it.faceIcon || "" return } } avatarPath = "" } Column { id: column anchors.centerIn: parent width: parent.width - 2 * 28 spacing: 16 Item { width: 96 height: 96 anchors.horizontalCenter: parent.horizontalCenter Rectangle { anchors.fill: parent radius: width / 2 color: config.stringValue("surface0") visible: avatarImage.status !== Image.Ready Text { anchors.centerIn: parent text: card.initials color: config.stringValue("text") font.family: "Noto Sans" font.pixelSize: 34 } } Image { id: avatarImage anchors.fill: parent source: card.avatarPath fillMode: Image.PreserveAspectCrop visible: false } MultiEffect { anchors.fill: parent source: avatarImage visible: avatarImage.status === Image.Ready maskEnabled: true maskSource: avatarMask } Rectangle { id: avatarMask anchors.fill: parent radius: width / 2 layer.enabled: true visible: false } Rectangle { anchors.fill: parent radius: width / 2 color: "transparent" border.width: 2 border.color: config.stringValue("accent") } } TextField { id: usernameField width: parent.width 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") } onTextChanged: card.refreshAvatar() onAccepted: card.login() Keys.onPressed: event => { card.wake() event.accepted = false } } TextField { id: passwordField width: parent.width 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") } onAccepted: card.login() Keys.onPressed: event => { card.wake() event.accepted = false } } Row { width: parent.width spacing: 8 ComboBox { id: sessionBox width: parent.width - loginButton.width - parent.spacing model: sessionModel textRole: "name" currentIndex: sessionModel.lastIndex font.family: "Noto Sans" 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") } } Button { id: loginButton text: "Log in" font.family: "Noto Sans" palette.button: config.stringValue("accent") palette.buttonText: config.stringValue("base") background: Rectangle { radius: 6 color: config.stringValue("accent") } contentItem: Text { text: loginButton.text color: config.stringValue("base") font: loginButton.font horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter padding: 8 } onClicked: card.login() } } Text { width: parent.width horizontalAlignment: Text.AlignHCenter wrapMode: Text.WordWrap font.family: "Noto Sans" font.pixelSize: 12 color: card.loginError !== "" ? config.stringValue("red") : config.stringValue("yellow") text: card.message } } function login() { if (usernameField.text.length === 0) return sddm.login(usernameField.text, passwordField.text, sessionBox.currentIndex) } // Login failure wins the notice line; otherwise Caps Lock, if on. property string loginError: "" readonly property string message: { if (loginError !== "") return loginError if (keyboard.capsLock) return "Caps Lock is on" return "" } Connections { target: sddm function onLoginFailed() { loginError = "Login failed" passwordField.text = "" shake.restart() } function onLoginSucceeded() { loginError = "" } } SequentialAnimation { id: shake NumberAnimation { target: card; property: "shakeOffset"; to: -10; duration: 40 } NumberAnimation { target: card; property: "shakeOffset"; to: 10; duration: 40 } NumberAnimation { target: card; property: "shakeOffset"; to: 0; duration: 40 } } Component.onCompleted: { refreshAvatar() usernameField.forceActiveFocus() } }