nix-flakes/hosts/machine/ena/sddm-theme/Main.qml
2026-09-14 12:09:52 +09:00

489 lines
15 KiB
QML

import QtQuick 2.15
import QtQuick.Window 2.15
Rectangle {
id: root
width: Screen.width
height: Screen.height
color: colors.background
QtObject {
id: colors
// Keep these in sync with the Mizuki Dark palette used by hyprlock.
readonly property color background: "#191017"
readonly property color black: "#000000"
readonly property color text: "#fafafa"
readonly property color pink: "#f5c2e7"
readonly property color secondary: "#bac2de"
readonly property color error: "#f38ba8"
readonly property color input: "#1affffff"
readonly property color inputActive: "#2affffff"
}
property int sessionIndex: Math.max(0, sessionModel.lastIndex)
property bool sessionMenuOpen: false
property bool loginInProgress: false
property string errorText: ""
property date now: new Date()
// Keep the interactive layout in the same coordinate system as hyprlock.
// Fixed logical margins prevent fractional scaling from pulling elements
// apart on the greeter screen.
property real edgeMargin: 32
property real loginBottomMargin: 120
function sessionCount() {
return sessionModel.rowCount();
}
function sessionName(index) {
if (index === undefined)
index = sessionIndex;
var count = sessionCount();
if (count === 0)
return "No session";
index = Math.max(0, Math.min(index, count - 1));
var label = sessionModel.data(sessionModel.index(index, 0), Qt.UserRole + 4);
return label ? label.toString() : "Desktop";
}
function selectSession(index) {
sessionIndex = index;
sessionMenuOpen = false;
}
function login() {
if (loginInProgress || username.text.trim().length === 0)
return;
errorText = "";
loginInProgress = true;
sddm.login(username.text.trim(), password.text, sessionIndex);
}
Timer {
interval: 1000
repeat: true
running: true
onTriggered: root.now = new Date()
}
// Hyprlock uses the current wallpaper with a dark, low-brightness treatment.
// SDDM cannot read the logged-in user's current-wallpaper symlink, so it uses
// the same default wallpaper shipped with this host.
Image {
anchors.fill: parent
source: Qt.resolvedUrl("wallpaper.png")
fillMode: Image.PreserveAspectCrop
asynchronous: true
cache: true
smooth: true
}
Rectangle {
anchors.fill: parent
color: colors.black
opacity: 0.40
}
Rectangle {
anchors.fill: parent
color: colors.background
opacity: 0.08
}
// One clock container owns all four labels. Keeping the labels in one
// column avoids independent anchor calculations drifting at fractional
// display scales, while the whole cluster stays above the login form.
Item {
id: clockCluster
width: 420
height: 360
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: -80
Column {
anchors.centerIn: parent
spacing: -18
Text {
width: clockCluster.width
text: Qt.formatTime(root.now, "HH")
color: colors.text
horizontalAlignment: Text.AlignHCenter
font.family: "Adwaita Sans"
font.pixelSize: 112
font.weight: Font.Bold
}
Text {
width: clockCluster.width
text: Qt.formatTime(root.now, "mm")
color: colors.text
horizontalAlignment: Text.AlignHCenter
font.family: "Adwaita Sans"
font.pixelSize: 112
font.weight: Font.Bold
}
Item { width: 1; height: 8 }
Text {
width: clockCluster.width
text: Qt.formatDate(root.now, "dddd")
color: colors.pink
horizontalAlignment: Text.AlignHCenter
font.family: "JetBrainsMono NFM"
font.pixelSize: 18
font.bold: true
}
Text {
width: clockCluster.width
text: Qt.formatDate(root.now, "dd MMM")
color: colors.pink
horizontalAlignment: Text.AlignHCenter
font.family: "JetBrainsMono NFM"
font.pixelSize: 14
font.bold: true
}
}
}
// User selection stays immediately above the password field, as requested.
Column {
id: loginArea
width: 250
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: root.loginBottomMargin
spacing: 10
Item {
width: parent.width
height: 60
Text {
anchors.left: parent.left
anchors.top: parent.top
text: "USER"
color: colors.pink
font.family: "JetBrainsMono NFM"
font.pixelSize: 10
font.letterSpacing: 1.2
}
Rectangle {
id: userField
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
height: 42
radius: 20
color: username.activeFocus ? colors.inputActive : colors.input
border.color: colors.pink
border.width: username.activeFocus ? 3 : 2
TextInput {
id: username
anchors.fill: parent
anchors.leftMargin: 16
anchors.rightMargin: 16
color: colors.text
selectionColor: colors.pink
selectedTextColor: colors.background
verticalAlignment: TextInput.AlignVCenter
font.family: "Adwaita Sans"
font.pixelSize: 16
text: userModel.lastUser
selectByMouse: true
clip: true
Keys.onPressed: function(event) {
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
password.forceActiveFocus();
event.accepted = true;
}
}
onTextChanged: root.errorText = ""
}
Text {
anchors.left: parent.left
anchors.leftMargin: 16
anchors.verticalCenter: parent.verticalCenter
text: "Username..."
color: colors.secondary
font.family: "Adwaita Sans"
font.pixelSize: 16
font.italic: true
visible: username.text.length === 0 && !username.activeFocus
}
}
}
Rectangle {
id: passwordField
width: parent.width
height: 50
radius: 22
color: password.activeFocus ? colors.inputActive : colors.input
border.color: colors.pink
border.width: 3
TextInput {
id: password
anchors.fill: parent
anchors.leftMargin: 16
anchors.rightMargin: 16
color: colors.pink
selectionColor: colors.pink
selectedTextColor: colors.background
verticalAlignment: TextInput.AlignVCenter
horizontalAlignment: TextInput.AlignHCenter
font.family: "Adwaita Sans"
font.pixelSize: 17
echoMode: TextInput.Password
passwordCharacter: "•"
selectByMouse: true
clip: true
Keys.onPressed: function(event) {
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
root.login();
event.accepted = true;
}
}
onTextChanged: root.errorText = ""
}
Text {
anchors.fill: parent
anchors.leftMargin: 16
anchors.rightMargin: 16
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
text: "Password..."
color: colors.secondary
font.family: "Adwaita Sans"
font.pixelSize: 16
font.italic: true
visible: password.text.length === 0 && !password.activeFocus
}
}
Text {
width: parent.width
height: visible ? implicitHeight : 0
text: root.errorText
color: colors.error
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
font.family: "Adwaita Sans"
font.pixelSize: 12
visible: root.errorText.length > 0
}
}
// A quiet, centered failure message does not change the lockscreen-like layout.
Connections {
target: sddm
function onLoginFailed() {
root.loginInProgress = false;
root.errorText = "That password did not work. Try again.";
password.text = "";
password.forceActiveFocus();
}
function onLoginSucceeded() {
root.loginInProgress = false;
}
function onInformationMessage(message) {
root.loginInProgress = false;
root.errorText = message;
}
}
// Bottom-left desktop/session selector.
Item {
id: sessionSelector
z: 100
width: 250
height: 58
anchors.left: parent.left
anchors.leftMargin: root.edgeMargin
anchors.bottom: parent.bottom
anchors.bottomMargin: root.edgeMargin
Text {
anchors.left: parent.left
anchors.top: parent.top
text: "SESSION"
color: colors.pink
font.family: "JetBrainsMono NFM"
font.pixelSize: 10
font.letterSpacing: 1.2
}
Rectangle {
id: sessionButton
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
height: 38
radius: 19
color: sessionMouse.containsMouse ? "#32ffffff" : "#1affffff"
border.color: colors.pink
border.width: 2
opacity: root.sessionMenuOpen ? 1 : 0.9
Text {
anchors.left: parent.left
anchors.leftMargin: 16
anchors.right: arrow.left
anchors.verticalCenter: parent.verticalCenter
text: root.sessionName()
color: colors.text
elide: Text.ElideRight
font.family: "Adwaita Sans"
font.pixelSize: 14
}
Text {
id: arrow
anchors.right: parent.right
anchors.rightMargin: 15
anchors.verticalCenter: parent.verticalCenter
text: root.sessionMenuOpen ? "⌃" : "⌄"
color: colors.pink
font.family: "Adwaita Sans"
font.pixelSize: 18
}
MouseArea {
id: sessionMouse
anchors.fill: parent
hoverEnabled: true
enabled: root.sessionCount() > 0
onClicked: root.sessionMenuOpen = !root.sessionMenuOpen
}
}
}
MouseArea {
id: sessionMenuDismiss
anchors.fill: parent
z: 110
visible: root.sessionMenuOpen
onClicked: root.sessionMenuOpen = false
}
Rectangle {
id: sessionMenu
z: 111
width: sessionSelector.width
height: Math.min(Math.max(46, root.sessionCount() * 46), root.height * 0.38)
anchors.left: sessionSelector.left
// sessionButton is inside sessionSelector, so anchor to the sibling
// container instead of crossing item hierarchies.
anchors.bottom: sessionSelector.top
anchors.bottomMargin: 8
radius: 18
color: "#f2191017"
border.color: colors.pink
border.width: 2
visible: root.sessionMenuOpen && root.sessionCount() > 0
clip: true
ListView {
id: sessionList
anchors.fill: parent
anchors.margins: 4
clip: true
model: root.sessionCount()
currentIndex: root.sessionIndex
delegate: Rectangle {
width: sessionList.width
height: 42
radius: 14
color: index === root.sessionIndex ? "#32f5c2e7" : (sessionDelegateMouse.containsMouse ? "#22f5c2e7" : "transparent")
Text {
anchors.left: parent.left
anchors.leftMargin: 14
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: root.sessionName(index)
color: colors.text
elide: Text.ElideRight
font.family: "Adwaita Sans"
font.pixelSize: 14
}
MouseArea {
id: sessionDelegateMouse
anchors.fill: parent
hoverEnabled: true
onClicked: root.selectSession(index)
}
}
}
}
// SDDM-only controls; the lock screen itself has no power controls.
Row {
z: 100
anchors.right: parent.right
anchors.rightMargin: root.edgeMargin
anchors.bottom: parent.bottom
anchors.bottomMargin: root.edgeMargin + 6
spacing: 22
Text {
text: "POWER OFF"
color: powerMouse.containsMouse ? colors.text : colors.secondary
font.family: "JetBrainsMono NFM"
font.pixelSize: 10
font.letterSpacing: 1.1
MouseArea {
id: powerMouse
anchors.fill: parent
anchors.margins: -10
hoverEnabled: true
onClicked: sddm.powerOff()
}
}
Text {
text: "REBOOT"
color: rebootMouse.containsMouse ? colors.text : colors.secondary
font.family: "JetBrainsMono NFM"
font.pixelSize: 10
font.letterSpacing: 1.1
MouseArea {
id: rebootMouse
anchors.fill: parent
anchors.margins: -10
hoverEnabled: true
onClicked: sddm.reboot()
}
}
}
Component.onCompleted: {
if (username.text.length > 0)
password.forceActiveFocus();
else
username.forceActiveFocus();
}
}