From b5e2e68a22ed9afbd305c59d2eae691c231bd095 Mon Sep 17 00:00:00 2001 From: bbedward Date: Mon, 22 Jun 2026 13:50:50 -0400 Subject: [PATCH] greeter: redesign the switch-user/auto-login flow - Fix visual glitches - Make auto-login and switch user texts not always visible - Make from-zero memory state not confusing --- quickshell/Modules/Greetd/GreeterContent.qml | 222 ++++++----------- .../Modules/Greetd/GreeterUserPicker.qml | 225 ++++++++++++------ 2 files changed, 222 insertions(+), 225 deletions(-) diff --git a/quickshell/Modules/Greetd/GreeterContent.qml b/quickshell/Modules/Greetd/GreeterContent.qml index c8abf5c1..81c8f354 100644 --- a/quickshell/Modules/Greetd/GreeterContent.qml +++ b/quickshell/Modules/Greetd/GreeterContent.qml @@ -73,7 +73,7 @@ Item { readonly property bool greeterPamHasExternalAuth: greeterPamHasFprint || greeterPamHasU2f readonly property bool multipleUsersAvailable: GreeterUsersService.loaded && GreeterUsersService.users.length > 1 readonly property bool showUserPicker: multipleUsersAvailable && !GreeterState.showPasswordInput && !manualUsernameEntry - readonly property bool showAccountSwitchLink: multipleUsersAvailable && !GreeterState.showPasswordInput && !GreeterState.unlocking + readonly property bool showAccountSwitchLink: multipleUsersAvailable && manualUsernameEntry && !GreeterState.showPasswordInput && !GreeterState.unlocking readonly property int userPickerMaxHeight: Math.min(400, Math.max(120, height * 0.35)) property bool userListOpen: false property bool manualUsernameEntry: false @@ -533,7 +533,6 @@ Item { passwordFailureCount = 0; clearAuthFeedback(); externalAuthAutoStartedForUser = ""; - root.autoLoginOnSuccess = false; } root.pickerThemeUsername = user; GreeterState.username = user; @@ -870,6 +869,13 @@ Item { anchors.fill: parent color: "transparent" + MouseArea { + anchors.fill: parent + enabled: root.userListOpen + visible: root.userListOpen + onClicked: root.userListOpen = false + } + Column { id: greeterMainColumn @@ -1006,17 +1012,6 @@ Item { opacity: 0.9 } - StyledText { - id: userPickerHint - - anchors.horizontalCenter: parent.horizontalCenter - visible: root.showUserPicker && !GreeterState.showPasswordInput && !GreeterState.username && !root.userListOpen - text: I18n.tr("Select user...", "greeter user picker placeholder") - font.pixelSize: Theme.fontSizeMedium - color: "white" - opacity: 0.85 - } - ColumnLayout { id: authColumn @@ -1055,7 +1050,7 @@ Item { radius: width / 2 color: "transparent" border.color: Theme.primary - border.width: avatarPickerArea.containsMouse || root.userListOpen ? 2 : 0 + border.width: (avatarPickerArea.containsMouse || root.userListOpen) && !GreeterState.showPasswordInput ? 2 : 0 visible: root.multipleUsersAvailable Behavior on border.width { NumberAnimation { @@ -1065,6 +1060,29 @@ Item { } } + // Switch-user affordance: hover scrim over the selected user's avatar. + Rectangle { + anchors.fill: parent + radius: width / 2 + color: Qt.rgba(0, 0, 0, 0.55) + opacity: (root.multipleUsersAvailable && GreeterState.showPasswordInput && avatarPickerArea.containsMouse) ? 1 : 0 + visible: opacity > 0 + + Behavior on opacity { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + + DankIcon { + anchors.centerIn: parent + name: "switch_account" + size: 24 + color: "white" + } + } + MouseArea { id: avatarPickerArea @@ -1089,6 +1107,7 @@ Item { Layout.fillWidth: true Layout.preferredHeight: root.showUserPicker && root.userListOpen ? Math.max(60, userPicker.implicitHeight + Theme.spacingM * 2) : 60 + clip: true radius: Theme.cornerRadius color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.9) border.color: inputField.activeFocus ? Theme.primary : Qt.rgba(1, 1, 1, 0.3) @@ -1105,8 +1124,13 @@ Item { maxExpandedHeight: root.userPickerMaxHeight visible: root.showUserPicker && !GreeterState.showPasswordInput expanded: root.userListOpen + autoLoginVisible: GreetdSettings.rememberLastUser && GreetdSettings.rememberLastSession + autoLoginChecked: root.autoLoginOnSuccess + manualEntryVisible: true onUserSelected: username => root.selectUser(username, false) onToggleRequested: root.userListOpen = !root.userListOpen + onAutoLoginToggled: root.autoLoginOnSuccess = !root.autoLoginOnSuccess + onManualEntryRequested: root.enterManualUsernameEntry() } DankIcon { @@ -1341,6 +1365,13 @@ Item { easing.type: Theme.standardEasing } } + + Behavior on Layout.preferredHeight { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.standardEasing + } + } } } @@ -1353,7 +1384,7 @@ Item { id: accountSwitchLabel anchors.horizontalCenter: parent.horizontalCenter - text: root.manualUsernameEntry ? I18n.tr("Back to user list", "greeter link to return from manual username entry to user picker") : I18n.tr("Not listed?", "greeter link to switch to manual username entry") + text: I18n.tr("Back to user list", "greeter link to return from manual username entry to user picker") color: Theme.primary font.pixelSize: Theme.fontSizeSmall font.underline: accountSwitchMouse.containsMouse @@ -1365,12 +1396,7 @@ Item { anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor - onClicked: { - if (root.manualUsernameEntry) - root.returnToUserListFromManualEntry(); - else - root.enterManualUsernameEntry(); - } + onClicked: root.returnToUserListFromManualEntry() } } @@ -1395,150 +1421,36 @@ Item { } } - // Password-screen actions: Switch User + Auto-login toggle as one compact chip row + // Single-user auto-login toggle: its only home, since there is no picker to host it. + // Multi-user switching lives on the avatar hover; multi-user auto-login lives in the picker. + // Height stays reserved during unlocking (fade only) so the centered column doesn't jump. Item { id: passwordActions readonly property bool autoLoginAvailable: GreetdSettings.rememberLastUser && GreetdSettings.rememberLastSession + readonly property bool showAutoLoginToggle: !root.multipleUsersAvailable && autoLoginAvailable Layout.fillWidth: true Layout.topMargin: Theme.spacingXS Layout.preferredHeight: visible ? 32 : 0 - visible: GreeterState.showPasswordInput && !GreeterState.unlocking && (root.multipleUsersAvailable || autoLoginAvailable) + visible: GreeterState.showPasswordInput && showAutoLoginToggle + opacity: GreeterState.unlocking ? 0 : 1 - Row { + Behavior on opacity { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + + DankActionButton { anchors.centerIn: parent - spacing: Theme.spacingS - - Rectangle { - id: switchUserChip - - visible: root.multipleUsersAvailable - height: 32 - width: switchUserContent.implicitWidth + Theme.spacingM * 2 - radius: height / 2 - color: Theme.withAlpha(Theme.surfaceVariant, 0.65) - - Rectangle { - anchors.fill: parent - radius: parent.radius - color: (switchUserMouse.containsMouse || switchUserMouse.pressed) ? Theme.surfaceTextHover : "transparent" - - Behavior on color { - ColorAnimation { - duration: Theme.shorterDuration - easing.type: Theme.standardEasing - } - } - } - - DankRipple { - id: switchUserRipple - cornerRadius: switchUserChip.radius - rippleColor: Theme.surfaceVariantText - } - - Row { - id: switchUserContent - anchors.centerIn: parent - spacing: Theme.spacingXS - - DankIcon { - name: "people" - size: 16 - color: Theme.surfaceVariantText - anchors.verticalCenter: parent.verticalCenter - } - - StyledText { - text: I18n.tr("Switch User") - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - anchors.verticalCenter: parent.verticalCenter - } - } - - MouseArea { - id: switchUserMouse - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onPressed: mouse => switchUserRipple.trigger(mouse.x, mouse.y) - onClicked: root.returnToUserPicker() - } - } - - Rectangle { - id: autoLoginChip - - visible: passwordActions.autoLoginAvailable - height: 32 - width: autoLoginContent.implicitWidth + Theme.spacingM * 2 - radius: height / 2 - color: root.autoLoginOnSuccess ? Theme.withAlpha(Theme.primary, 0.85) : Theme.withAlpha(Theme.surfaceVariant, 0.65) - - Behavior on color { - ColorAnimation { - duration: Theme.shortDuration - easing.type: Theme.standardEasing - } - } - - Rectangle { - anchors.fill: parent - radius: parent.radius - color: { - if (autoLoginMouse.pressed) - return root.autoLoginOnSuccess ? Theme.primaryPressed : Theme.surfaceTextHover; - if (autoLoginMouse.containsMouse) - return root.autoLoginOnSuccess ? Theme.primaryHover : Theme.surfaceTextHover; - return "transparent"; - } - - Behavior on color { - ColorAnimation { - duration: Theme.shorterDuration - easing.type: Theme.standardEasing - } - } - } - - DankRipple { - id: autoLoginRipple - cornerRadius: autoLoginChip.radius - rippleColor: root.autoLoginOnSuccess ? Theme.primaryText : Theme.surfaceVariantText - } - - Row { - id: autoLoginContent - anchors.centerIn: parent - spacing: Theme.spacingXS - - DankIcon { - name: root.autoLoginOnSuccess ? "check" : "login" - size: 16 - color: root.autoLoginOnSuccess ? Theme.primaryText : Theme.surfaceVariantText - anchors.verticalCenter: parent.verticalCenter - } - - StyledText { - text: I18n.tr("Auto-login") - font.pixelSize: Theme.fontSizeSmall - font.weight: root.autoLoginOnSuccess ? Font.Medium : Font.Normal - color: root.autoLoginOnSuccess ? Theme.primaryText : Theme.surfaceVariantText - anchors.verticalCenter: parent.verticalCenter - } - } - - MouseArea { - id: autoLoginMouse - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: root.autoLoginOnSuccess = !root.autoLoginOnSuccess - onPressed: mouse => autoLoginRipple.trigger(mouse.x, mouse.y) - } - } + iconName: root.autoLoginOnSuccess ? "check_box" : "check_box_outline_blank" + iconSize: 18 + buttonSize: 32 + iconColor: root.autoLoginOnSuccess ? Theme.primary : Qt.rgba(1, 1, 1, 0.55) + tooltipText: I18n.tr("Auto-login") + onClicked: root.autoLoginOnSuccess = !root.autoLoginOnSuccess } } } diff --git a/quickshell/Modules/Greetd/GreeterUserPicker.qml b/quickshell/Modules/Greetd/GreeterUserPicker.qml index b0d55501..cc53cd62 100644 --- a/quickshell/Modules/Greetd/GreeterUserPicker.qml +++ b/quickshell/Modules/Greetd/GreeterUserPicker.qml @@ -7,22 +7,38 @@ import qs.Widgets Item { id: root + LayoutMirroring.enabled: I18n.isRtl + LayoutMirroring.childrenInherit: true + property bool expanded: false property int maxExpandedHeight: 400 + property bool autoLoginVisible: false + property bool autoLoginChecked: false + property bool manualEntryVisible: false signal userSelected(string username) - signal toggleRequested() + signal toggleRequested + signal autoLoginToggled + signal manualEntryRequested readonly property int rowHeight: 52 readonly property int collapsedBarHeight: 36 - readonly property int expandedListHeight: { - if (!expanded) - return 0; + readonly property int actionRowHeight: 44 + + readonly property int userListFullHeight: { const count = GreeterUsersService.users.length; if (count === 0) return 0; - const fullHeight = count * rowHeight + Math.max(0, count - 1) * Theme.spacingXS; - return Math.min(maxExpandedHeight, fullHeight); + return count * rowHeight + Math.max(0, count - 1) * Theme.spacingXS; + } + readonly property int manualEntryBlockHeight: manualEntryVisible ? actionRowHeight + Theme.spacingXS : 0 + readonly property int autoLoginBlockHeight: autoLoginVisible ? actionRowHeight + Theme.spacingXS : 0 + readonly property int expandedContentHeight: { + if (!expanded) + return 0; + if (GreeterUsersService.users.length === 0 && !autoLoginVisible && !manualEntryVisible) + return 0; + return Math.min(maxExpandedHeight, userListFullHeight + manualEntryBlockHeight + autoLoginBlockHeight); } function encodeFileUrl(path) { @@ -38,49 +54,34 @@ Item { return ""; } - implicitHeight: expanded ? expandedListHeight : collapsedBarHeight + implicitHeight: expanded ? expandedContentHeight : collapsedBarHeight implicitWidth: parent ? parent.width : 320 - RowLayout { - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: expanded ? undefined : parent.verticalCenter - height: collapsedBarHeight - visible: !expanded && !!GreeterState.username - spacing: Theme.spacingM - - StyledText { - Layout.fillWidth: true - text: GreeterUsersService.optionLabel(GreeterState.username) - color: Theme.surfaceText - font.pixelSize: Theme.fontSizeMedium - elide: Text.ElideRight - } - - DankIcon { - name: "expand_more" - size: 20 - color: Theme.surfaceVariantText - } - - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: root.toggleRequested() - } - } - Item { anchors.left: parent.left anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter height: collapsedBarHeight - visible: !expanded && !GreeterState.username + visible: !expanded - DankIcon { - anchors.centerIn: parent - name: "expand_more" - size: 20 - color: Theme.surfaceVariantText + RowLayout { + anchors.fill: parent + spacing: Theme.spacingM + + StyledText { + Layout.fillWidth: true + text: GreeterState.username ? GreeterUsersService.optionLabel(GreeterState.username) : I18n.tr("Select user...", "greeter user picker placeholder") + color: GreeterState.username ? Theme.surfaceText : Theme.surfaceVariantText + font.pixelSize: Theme.fontSizeMedium + elide: Text.ElideRight + } + + DankIcon { + Layout.alignment: Qt.AlignVCenter + name: "expand_more" + size: 20 + color: Theme.surfaceVariantText + } } MouseArea { @@ -90,31 +91,80 @@ Item { } } - DankListView { - id: userListView - + Column { anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top - height: expandedListHeight + height: root.expandedContentHeight visible: expanded - clip: true - interactive: contentHeight > height spacing: Theme.spacingXS - model: GreeterUsersService.users - delegate: Rectangle { - id: userRow + DankListView { + id: userListView - required property var modelData - required property int index + width: parent.width + height: parent.height - root.manualEntryBlockHeight - root.autoLoginBlockHeight + clip: true + interactive: contentHeight > height + spacing: Theme.spacingXS + model: GreeterUsersService.users - width: userListView.width - height: root.rowHeight + delegate: Rectangle { + id: userRow + + required property var modelData + required property int index + + width: userListView.width + height: root.rowHeight + radius: Theme.cornerRadius + color: userRowMouse.containsMouse ? Theme.surfacePressed : "transparent" + border.color: GreeterState.username === userRow.modelData.username ? Theme.primary : "transparent" + border.width: GreeterState.username === userRow.modelData.username ? 1 : 0 + + RowLayout { + anchors.fill: parent + anchors.leftMargin: Theme.spacingS + anchors.rightMargin: Theme.spacingS + spacing: Theme.spacingM + + Item { + Layout.preferredWidth: 36 + Layout.preferredHeight: 36 + + DankCircularImage { + anchors.fill: parent + imageSource: root.profileImageSource(userRow.modelData.username) + fallbackIcon: "person" + } + } + + StyledText { + Layout.fillWidth: true + text: GreeterUsersService.optionLabel(userRow.modelData.username) + color: Theme.surfaceText + font.pixelSize: Theme.fontSizeMedium + elide: Text.ElideRight + } + } + + MouseArea { + id: userRowMouse + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: root.userSelected(userRow.modelData.username) + } + } + } + + Rectangle { + width: parent.width + height: root.actionRowHeight + visible: root.manualEntryVisible radius: Theme.cornerRadius - color: userRowMouse.containsMouse ? Theme.surfacePressed : "transparent" - border.color: GreeterState.username === userRow.modelData.username ? Theme.primary : "transparent" - border.width: GreeterState.username === userRow.modelData.username ? 1 : 0 + color: manualEntryRowMouse.containsMouse ? Theme.surfacePressed : "transparent" RowLayout { anchors.fill: parent @@ -122,20 +172,16 @@ Item { anchors.rightMargin: Theme.spacingS spacing: Theme.spacingM - Item { - Layout.preferredWidth: 36 - Layout.preferredHeight: 36 - - DankCircularImage { - anchors.fill: parent - imageSource: root.profileImageSource(userRow.modelData.username) - fallbackIcon: "person" - } + DankIcon { + Layout.alignment: Qt.AlignVCenter + name: "person_add" + size: 20 + color: Theme.surfaceVariantText } StyledText { Layout.fillWidth: true - text: GreeterUsersService.optionLabel(userRow.modelData.username) + text: I18n.tr("Not listed?", "greeter link to switch to manual username entry") color: Theme.surfaceText font.pixelSize: Theme.fontSizeMedium elide: Text.ElideRight @@ -143,12 +189,51 @@ Item { } MouseArea { - id: userRowMouse + id: manualEntryRowMouse anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor - onClicked: root.userSelected(userRow.modelData.username) + onClicked: root.manualEntryRequested() + } + } + + Rectangle { + width: parent.width + height: root.actionRowHeight + visible: root.autoLoginVisible + radius: Theme.cornerRadius + color: autoLoginRowMouse.containsMouse ? Theme.surfacePressed : "transparent" + + RowLayout { + anchors.fill: parent + anchors.leftMargin: Theme.spacingS + anchors.rightMargin: Theme.spacingS + spacing: Theme.spacingM + + DankIcon { + Layout.alignment: Qt.AlignVCenter + name: root.autoLoginChecked ? "check_box" : "check_box_outline_blank" + size: 20 + color: root.autoLoginChecked ? Theme.primary : Theme.surfaceVariantText + } + + StyledText { + Layout.fillWidth: true + text: I18n.tr("Auto-login") + color: Theme.surfaceText + font.pixelSize: Theme.fontSizeMedium + elide: Text.ElideRight + } + } + + MouseArea { + id: autoLoginRowMouse + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: root.autoLoginToggled() } } }