aboutsummaryrefslogtreecommitdiffstats
path: root/theme/components/Panel.qml
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-09-17 14:28:41 +0200
committerDanilo M. <danix@danix.xyz>2026-09-17 14:28:41 +0200
commit8ead58fb5baa8d781159cf443c0f7ffd68cb6775 (patch)
tree83e64a459dd45e028b7073f1d5cc2dafcfd05252 /theme/components/Panel.qml
parentcdcf62416feffcb81cb07fdfe2e2dc0320490829 (diff)
downloadsddm-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.qml273
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()
+ }
+}