diff options
| author | Danilo M. <danix@danix.xyz> | 2026-09-17 14:28:41 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-09-17 14:28:41 +0200 |
| commit | 8ead58fb5baa8d781159cf443c0f7ffd68cb6775 (patch) | |
| tree | 83e64a459dd45e028b7073f1d5cc2dafcfd05252 /theme/components/Panel.qml | |
| parent | cdcf62416feffcb81cb07fdfe2e2dc0320490829 (diff) | |
| download | sddm-theme-udt-8ead58fb5baa8d781159cf443c0f7ffd68cb6775.tar.gz sddm-theme-udt-8ead58fb5baa8d781159cf443c0f7ffd68cb6775.zip | |
feat: replace the login card with a left panel
Draw the greeter as a full-height left panel with a clock, greeting,
avatar, a staged username-then-password slot, a power row and the
session selector at the bottom. Delete the centered login card.
Diffstat (limited to 'theme/components/Panel.qml')
| -rw-r--r-- | theme/components/Panel.qml | 273 |
1 files changed, 273 insertions, 0 deletions
diff --git a/theme/components/Panel.qml b/theme/components/Panel.qml new file mode 100644 index 0000000..4df989b --- /dev/null +++ b/theme/components/Panel.qml @@ -0,0 +1,273 @@ +// SPDX-License-Identifier: GPL-2.0-only +// Copyright (C) 2026 Danilo M. <danix@danix.xyz> +// +// 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: true + 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: 56 + font.weight: Font.Light + } + + Text { + width: parent.width + horizontalAlignment: Text.AlignHCenter + text: Qt.formatDateTime(panel.now, panel.dateFormat) + color: panel.subtextColor + font.family: "Noto Sans" + font.pixelSize: 18 + } + + Text { + width: parent.width + horizontalAlignment: Text.AlignHCenter + text: panel.greeting + color: panel.textColor + font.family: "Noto Sans" + font.pixelSize: 18 + topPadding: 8 + } + + 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: panel.initials + color: panel.textColor + font.family: "Noto Sans" + font.pixelSize: 34 + } + } + + 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.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() + } +} |
