// SPDX-License-Identifier: GPL-2.0-only // Copyright (C) 2026 Danilo M. // // The left login panel: clock and date, greeting, avatar, the staged credential // slot, the power row and the session selector. Main shows it on the window // whose screen matches uiScreen, else on the primary one. import QtQuick import QtQuick.Controls.Basic import QtQuick.Effects Item { id: panel signal wake() // The wallpaper image to frost behind the panel, and the failed-login shake // applied to the content so the frost stays put. property Item backdropSource: null property real shakeOffset: 0 readonly property string accentColor: config.stringValue("accent") readonly property string textColor: config.stringValue("text") readonly property string subtextColor: config.stringValue("subtext0") readonly property string greeting: config.stringValue("greeting") !== "" ? config.stringValue("greeting") : "hello there" readonly property string timeFormat: config.stringValue("timeFormat") !== "" ? config.stringValue("timeFormat") : "HH:mm" readonly property string dateFormat: config.stringValue("dateFormat") !== "" ? config.stringValue("dateFormat") : "dddd, d MMMM" // Live clock, ticked once a second. property date now: new Date() Timer { interval: 1000 running: panel.visible repeat: true onTriggered: panel.now = new Date() } // Frosted ground, square corners. Backdrop { anchors.fill: parent radius: 0 sourceItem: panel.backdropSource tint: config.stringValue("mantle") blurEnabled: config.boolValue("blur") } Rectangle { anchors.fill: parent color: "transparent" border.width: 2 border.color: panel.accentColor } // Non-visual: userModel faces, looked up by the entered username. Repeater { id: faces model: userModel delegate: Item { readonly property string faceName: name readonly property string faceIcon: icon visible: false } } property string avatarPath: "" readonly property string initials: creds.username.length > 0 ? creds.username.charAt(0).toUpperCase() : "?" function refreshAvatar() { for (var i = 0; i < faces.count; ++i) { var it = faces.itemAt(i) if (it && it.faceName === creds.username) { avatarPath = it.faceIcon || "" return } } avatarPath = "" } // Everything below shifts on a failed login. Item { id: content anchors.fill: parent transform: Translate { x: panel.shakeOffset } Column { id: top anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right anchors.margins: 32 spacing: 14 Text { width: parent.width horizontalAlignment: Text.AlignHCenter text: Qt.formatDateTime(panel.now, panel.timeFormat) color: panel.textColor font.family: "Noto Sans" font.pixelSize: 140 font.weight: Font.Bold } Text { width: parent.width horizontalAlignment: Text.AlignLeft text: Qt.formatDateTime(panel.now, panel.dateFormat).toUpperCase() color: panel.subtextColor font.family: "Noto Sans" font.pixelSize: 20 } Text { width: parent.width horizontalAlignment: Text.AlignHCenter text: panel.greeting color: panel.textColor font.family: "Noto Sans" font.pixelSize: 48 wrapMode: Text.WordWrap topPadding: 8 } Item { width: 256 height: 256 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: panel.initials color: panel.textColor font.family: "Noto Sans" font.pixelSize: 92 } } Image { id: avatarImage anchors.fill: parent source: panel.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: panel.accentColor } } Credentials { id: creds width: parent.width onSubmitted: panel.submit() onWake: panel.wake() onUsernameChanged: panel.refreshAvatar() } Text { width: parent.width horizontalAlignment: Text.AlignHCenter wrapMode: Text.WordWrap font.family: "Noto Sans" font.pixelSize: 12 color: panel.loginError !== "" ? config.stringValue("red") : config.stringValue("yellow") text: panel.message } } PowerBar { anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: sessionBox.top anchors.bottomMargin: 20 } ComboBox { id: sessionBox anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom anchors.margins: 32 model: sessionModel textRole: "name" currentIndex: sessionModel.lastIndex font.family: "Noto Sans" palette.base: config.stringValue("surface0") palette.text: config.stringValue("text") palette.button: config.stringValue("surface0") palette.buttonText: config.stringValue("text") palette.window: config.stringValue("surface0") palette.placeholderText: config.stringValue("overlay0") palette.mid: config.stringValue("overlay1") palette.dark: config.stringValue("subtext0") palette.highlight: config.stringValue("accent") palette.highlightedText: config.stringValue("base") background: Rectangle { radius: 6 color: config.stringValue("surface0") } } } function submit() { if (creds.username.length === 0) return sddm.login(creds.username, creds.password, sessionBox.currentIndex) } function restoreFocus() { creds.focusStage() } // Login failure wins the notice line, otherwise Caps Lock. 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() { panel.loginError = "Login failed" creds.clearPassword() shake.restart() } function onLoginSucceeded() { panel.loginError = "" } } SequentialAnimation { id: shake NumberAnimation { target: panel; property: "shakeOffset"; to: -10; duration: 40 } NumberAnimation { target: panel; property: "shakeOffset"; to: 10; duration: 40 } NumberAnimation { target: panel; property: "shakeOffset"; to: 0; duration: 40 } } Component.onCompleted: { refreshAvatar() creds.focusStage() } }