feat: expressive lock input (#1536)
* feat: shapes in lock input * fix: better lock input placeholder * feat: scale lock center properly + style improvements * fix: use non anim placeholder width * fix: not mono lock state text * fix: use shape queue instead of random * fix: use scale for enter button instead of state layer * fix: no slant for lock state message * fix: enter button pressable when no buffer
This commit is contained in:
parent
63b522648e
commit
a1124c8228
4 changed files with 229 additions and 104 deletions
|
|
@ -41,6 +41,7 @@ ColumnLayout {
|
||||||
|
|
||||||
PasswordInput {
|
PasswordInput {
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
centerScale: Math.max(0.8, root.centerScale)
|
||||||
centerWidth: root.centerWidth
|
centerWidth: root.centerWidth
|
||||||
lock: root.lock
|
lock: root.lock
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
|
import M3Shapes
|
||||||
import Caelestia.Config
|
import Caelestia.Config
|
||||||
import qs.components
|
import qs.components
|
||||||
import qs.services
|
import qs.services
|
||||||
|
|
@ -10,9 +11,19 @@ import qs.modules.lock
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
required property real centerScale
|
||||||
required property Pam pam
|
required property Pam pam
|
||||||
readonly property alias placeholder: placeholder
|
readonly property alias placeholder: placeholder
|
||||||
|
readonly property alias placeholderWidth: nonAnimPlaceholder.width
|
||||||
property string buffer
|
property string buffer
|
||||||
|
readonly property list<int> shapeQueue: {
|
||||||
|
const shapes = [MaterialShape.Slanted, MaterialShape.Arch, MaterialShape.Fan, MaterialShape.Arrow, MaterialShape.SemiCircle, MaterialShape.Triangle, MaterialShape.Diamond, MaterialShape.ClamShell, MaterialShape.Pentagon, MaterialShape.Gem, MaterialShape.Sunny, MaterialShape.VerySunny, MaterialShape.Cookie4Sided, MaterialShape.Ghostish, MaterialShape.SoftBurst];
|
||||||
|
for (let i = shapes.length - 1; i > 0; i--) {
|
||||||
|
const j = Math.floor(Math.random() * (i + 1));
|
||||||
|
[shapes[i], shapes[j]] = [shapes[j], shapes[i]];
|
||||||
|
}
|
||||||
|
return shapes;
|
||||||
|
}
|
||||||
|
|
||||||
clip: true
|
clip: true
|
||||||
|
|
||||||
|
|
@ -31,22 +42,30 @@ Item {
|
||||||
target: root.pam
|
target: root.pam
|
||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
TextMetrics {
|
||||||
id: placeholder
|
id: nonAnimPlaceholder
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
|
|
||||||
text: {
|
text: {
|
||||||
if (root.pam.passwd.active)
|
if (root.pam.passwd.active)
|
||||||
return qsTr("Loading...");
|
return qsTr("Loading...");
|
||||||
if (root.pam.state === "max")
|
if (root.pam.state === "max")
|
||||||
return qsTr("You have reached the maximum number of tries");
|
return qsTr("Max tries reached");
|
||||||
return qsTr("Enter your password");
|
return qsTr("Enter your password");
|
||||||
}
|
}
|
||||||
|
font: placeholder.font
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: placeholder
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
anchors.verticalCenterOffset: 1
|
||||||
|
|
||||||
|
text: nonAnimPlaceholder.text
|
||||||
|
|
||||||
animate: true
|
animate: true
|
||||||
color: root.pam.passwd.active ? Colours.palette.m3secondary : Colours.palette.m3outline
|
color: root.pam.passwd.active ? Colours.palette.m3secondary : Colours.palette.m3outline
|
||||||
font: Tokens.font.mono.medium
|
font: Tokens.font.body.builders.medium.scale(root.centerScale).width(110).build()
|
||||||
|
|
||||||
opacity: root.buffer ? 0 : 1
|
opacity: root.buffer ? 0 : 1
|
||||||
|
|
||||||
|
|
@ -60,7 +79,12 @@ Item {
|
||||||
ListView {
|
ListView {
|
||||||
id: charList
|
id: charList
|
||||||
|
|
||||||
readonly property int fullWidth: count * (implicitHeight + spacing) - spacing
|
readonly property int fullWidth: {
|
||||||
|
let w = (count - 1) * spacing;
|
||||||
|
for (let i = 0; i < count; i++)
|
||||||
|
w += ((itemAtIndex(i) as CharItem)?.nonAnimWidthScale ?? 1) * implicitHeight;
|
||||||
|
return w + implicitHeight; // Extra padding at ends
|
||||||
|
}
|
||||||
|
|
||||||
function bindImWidth(): void {
|
function bindImWidth(): void {
|
||||||
imWidthBehavior.enabled = false;
|
imWidthBehavior.enabled = false;
|
||||||
|
|
@ -82,63 +106,7 @@ Item {
|
||||||
values: root.buffer.split("")
|
values: root.buffer.split("")
|
||||||
}
|
}
|
||||||
|
|
||||||
delegate: StyledRect {
|
delegate: CharItem {}
|
||||||
id: ch
|
|
||||||
|
|
||||||
implicitWidth: implicitHeight
|
|
||||||
implicitHeight: charList.implicitHeight
|
|
||||||
|
|
||||||
color: Colours.palette.m3onSurface
|
|
||||||
radius: Tokens.rounding.medium / 2
|
|
||||||
|
|
||||||
opacity: 0
|
|
||||||
scale: 0
|
|
||||||
Component.onCompleted: {
|
|
||||||
opacity = 1;
|
|
||||||
scale = 1;
|
|
||||||
}
|
|
||||||
ListView.onRemove: removeAnim.start()
|
|
||||||
|
|
||||||
SequentialAnimation {
|
|
||||||
id: removeAnim
|
|
||||||
|
|
||||||
PropertyAction {
|
|
||||||
target: ch
|
|
||||||
property: "ListView.delayRemove"
|
|
||||||
value: true
|
|
||||||
}
|
|
||||||
ParallelAnimation {
|
|
||||||
Anim {
|
|
||||||
type: Anim.DefaultEffects
|
|
||||||
target: ch
|
|
||||||
property: "opacity"
|
|
||||||
to: 0
|
|
||||||
}
|
|
||||||
Anim {
|
|
||||||
target: ch
|
|
||||||
property: "scale"
|
|
||||||
to: 0.5
|
|
||||||
}
|
|
||||||
}
|
|
||||||
PropertyAction {
|
|
||||||
target: ch
|
|
||||||
property: "ListView.delayRemove"
|
|
||||||
value: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
Anim {
|
|
||||||
type: Anim.DefaultEffects
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on scale {
|
|
||||||
Anim {
|
|
||||||
type: Anim.FastSpatial
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on implicitWidth {
|
Behavior on implicitWidth {
|
||||||
id: imWidthBehavior
|
id: imWidthBehavior
|
||||||
|
|
@ -146,4 +114,121 @@ Item {
|
||||||
Anim {}
|
Anim {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
component CharItem: Item {
|
||||||
|
id: char
|
||||||
|
|
||||||
|
required property int index
|
||||||
|
property real nonAnimWidthScale: 1
|
||||||
|
|
||||||
|
implicitHeight: charList.implicitHeight
|
||||||
|
|
||||||
|
ListView.onRemove: {
|
||||||
|
initAnim.stop();
|
||||||
|
removeAnim.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
MaterialShape {
|
||||||
|
id: charShape
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
implicitSize: charList.implicitHeight * 1.5
|
||||||
|
shape: root.shapeQueue[char.index % root.shapeQueue.length] ?? MaterialShape.Circle
|
||||||
|
color: Colours.palette.m3onSurface
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
CAnim {}
|
||||||
|
}
|
||||||
|
|
||||||
|
SequentialAnimation {
|
||||||
|
id: initAnim
|
||||||
|
|
||||||
|
running: true
|
||||||
|
|
||||||
|
ParallelAnimation {
|
||||||
|
Anim {
|
||||||
|
target: charShape
|
||||||
|
property: "opacity"
|
||||||
|
from: 0
|
||||||
|
to: 1
|
||||||
|
type: Anim.DefaultEffects
|
||||||
|
}
|
||||||
|
Anim {
|
||||||
|
target: charShape
|
||||||
|
property: "scale"
|
||||||
|
from: 0
|
||||||
|
to: 1
|
||||||
|
type: Anim.FastSpatial
|
||||||
|
}
|
||||||
|
Anim {
|
||||||
|
target: char
|
||||||
|
property: "implicitWidth"
|
||||||
|
from: charList.implicitHeight
|
||||||
|
to: charList.implicitHeight * 1.3
|
||||||
|
type: Anim.DefaultEffects
|
||||||
|
}
|
||||||
|
PropertyAction {
|
||||||
|
target: char
|
||||||
|
property: "nonAnimWidthScale"
|
||||||
|
value: 1.5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PauseAnimation {
|
||||||
|
duration: 180 * Tokens.anim.durations.scale
|
||||||
|
}
|
||||||
|
PropertyAction {
|
||||||
|
target: charShape
|
||||||
|
property: "shape"
|
||||||
|
value: MaterialShape.Circle
|
||||||
|
}
|
||||||
|
ParallelAnimation {
|
||||||
|
Anim {
|
||||||
|
target: charShape
|
||||||
|
property: "scale"
|
||||||
|
to: 2 / 3
|
||||||
|
type: Anim.FastSpatial
|
||||||
|
}
|
||||||
|
Anim {
|
||||||
|
target: char
|
||||||
|
property: "implicitWidth"
|
||||||
|
to: charList.implicitHeight
|
||||||
|
type: Anim.DefaultEffects
|
||||||
|
}
|
||||||
|
PropertyAction {
|
||||||
|
target: char
|
||||||
|
property: "nonAnimWidthScale"
|
||||||
|
value: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SequentialAnimation {
|
||||||
|
id: removeAnim
|
||||||
|
|
||||||
|
PropertyAction {
|
||||||
|
target: char
|
||||||
|
property: "ListView.delayRemove"
|
||||||
|
value: true
|
||||||
|
}
|
||||||
|
ParallelAnimation {
|
||||||
|
Anim {
|
||||||
|
type: Anim.DefaultEffects
|
||||||
|
target: charShape
|
||||||
|
property: "opacity"
|
||||||
|
to: 0
|
||||||
|
}
|
||||||
|
Anim {
|
||||||
|
target: charShape
|
||||||
|
property: "scale"
|
||||||
|
to: 0.5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PropertyAction {
|
||||||
|
target: char
|
||||||
|
property: "ListView.delayRemove"
|
||||||
|
value: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
|
import M3Shapes
|
||||||
import Caelestia.Config
|
import Caelestia.Config
|
||||||
import qs.components
|
import qs.components
|
||||||
import qs.components.controls
|
import qs.components.controls
|
||||||
|
|
@ -8,10 +11,14 @@ import qs.services
|
||||||
StyledRect {
|
StyledRect {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
required property real centerScale
|
||||||
required property int centerWidth
|
required property int centerWidth
|
||||||
required property var lock
|
required property var lock
|
||||||
|
|
||||||
implicitWidth: centerWidth * 0.8
|
implicitWidth: {
|
||||||
|
const w = centerWidth * 0.8;
|
||||||
|
return lock.pam.buffer ? w : Math.min(w, inputField.placeholderWidth + iconWrapper.implicitWidth + enterButton.implicitWidth + input.spacing * 2 + Tokens.padding.medium * 2);
|
||||||
|
}
|
||||||
implicitHeight: input.implicitHeight + Tokens.padding.small
|
implicitHeight: input.implicitHeight + Tokens.padding.small
|
||||||
|
|
||||||
color: Colours.tPalette.m3surfaceContainer
|
color: Colours.tPalette.m3surfaceContainer
|
||||||
|
|
@ -33,6 +40,10 @@ StyledRect {
|
||||||
root.lock.pam.handleKey(event);
|
root.lock.pam.handleKey(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Behavior on implicitWidth {
|
||||||
|
Anim {}
|
||||||
|
}
|
||||||
|
|
||||||
StateLayer {
|
StateLayer {
|
||||||
hoverEnabled: false
|
hoverEnabled: false
|
||||||
cursorShape: Qt.IBeamCursor
|
cursorShape: Qt.IBeamCursor
|
||||||
|
|
@ -47,44 +58,39 @@ StyledRect {
|
||||||
spacing: Tokens.spacing.medium
|
spacing: Tokens.spacing.medium
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
|
id: iconWrapper
|
||||||
|
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
implicitWidth: height
|
implicitWidth: height
|
||||||
|
|
||||||
MaterialIcon {
|
AnimLoader {
|
||||||
id: fprintIcon
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
animate: true
|
anchors.verticalCenterOffset: sourceComponent === iconComp ? 1 : 0
|
||||||
text: {
|
sourceComp: root.lock.pam.passwd.active ? loadingComp : iconComp
|
||||||
if (root.lock.pam.fprint.tries >= GlobalConfig.lock.maxFprintTries)
|
}
|
||||||
return "fingerprint_off";
|
|
||||||
if (root.lock.pam.fprint.active)
|
|
||||||
return "fingerprint";
|
|
||||||
return "lock";
|
|
||||||
}
|
|
||||||
color: root.lock.pam.fprint.tries >= GlobalConfig.lock.maxFprintTries ? Colours.palette.m3error : Colours.palette.m3onSurface
|
|
||||||
opacity: root.lock.pam.passwd.active ? 0 : 1
|
|
||||||
|
|
||||||
Behavior on opacity {
|
Component {
|
||||||
Anim {
|
id: iconComp
|
||||||
type: Anim.DefaultEffects
|
|
||||||
|
MaterialIcon {
|
||||||
|
animate: true
|
||||||
|
text: {
|
||||||
|
if (root.lock.pam.fprint.tries >= GlobalConfig.lock.maxFprintTries)
|
||||||
|
return "fingerprint_off";
|
||||||
|
if (root.lock.pam.fprint.active)
|
||||||
|
return "fingerprint";
|
||||||
|
return "lock";
|
||||||
}
|
}
|
||||||
|
color: root.lock.pam.fprint.tries >= GlobalConfig.lock.maxFprintTries ? Colours.palette.m3error : Colours.palette.m3onSurfaceVariant
|
||||||
|
fontStyle: Tokens.font.icon.builders.medium.scale(root.centerScale).build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Loader {
|
Component {
|
||||||
anchors.fill: parent
|
id: loadingComp
|
||||||
anchors.margins: Tokens.padding.small
|
|
||||||
|
|
||||||
active: opacity > 0
|
LoadingIndicator {
|
||||||
opacity: root.lock.pam.passwd.active ? 1 : 0
|
implicitSize: iconWrapper.height - Tokens.padding.small * 2
|
||||||
|
|
||||||
sourceComponent: LoadingIndicator {}
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
Anim {
|
|
||||||
type: Anim.DefaultEffects
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -95,19 +101,45 @@ StyledRect {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
|
|
||||||
|
centerScale: root.centerScale
|
||||||
pam: root.lock.pam
|
pam: root.lock.pam
|
||||||
}
|
}
|
||||||
|
|
||||||
StyledRect {
|
Item {
|
||||||
|
id: enterButton
|
||||||
|
|
||||||
implicitWidth: implicitHeight
|
implicitWidth: implicitHeight
|
||||||
implicitHeight: enterIcon.implicitHeight + Tokens.padding.small
|
implicitHeight: {
|
||||||
|
const h = enterIcon.implicitHeight + Tokens.padding.extraSmall * 2;
|
||||||
|
return h % 2 === 0 ? h : h + 1;
|
||||||
|
}
|
||||||
|
|
||||||
color: root.lock.pam.buffer ? Colours.palette.m3primary : Colours.layer(Colours.palette.m3surfaceContainerHigh, 2)
|
MaterialShape {
|
||||||
radius: Tokens.rounding.full
|
anchors.fill: parent
|
||||||
|
|
||||||
StateLayer {
|
color: root.lock.pam.buffer ? Colours.palette.m3primary : Colours.layer(Colours.palette.m3surfaceContainerHigh, 2)
|
||||||
color: root.lock.pam.buffer ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface
|
shape: root.lock.pam.buffer ? MaterialShape.Arrow : MaterialShape.Circle
|
||||||
onClicked: root.lock.pam.passwd.start()
|
scale: !root.lock.pam.buffer ? 1 : mouse.pressed ? 0.6 : mouse.containsMouse ? 0.8 : 0.7
|
||||||
|
rotation: 90
|
||||||
|
|
||||||
|
Behavior on scale {
|
||||||
|
Anim {
|
||||||
|
type: Anim.FastSpatial
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
CAnim {}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: mouse
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: root.lock.pam.buffer ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||||
|
onClicked: root.lock.pam.buffer && root.lock.pam.passwd.start()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MaterialIcon {
|
MaterialIcon {
|
||||||
|
|
@ -115,8 +147,15 @@ StyledRect {
|
||||||
|
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: "arrow_forward"
|
text: "arrow_forward"
|
||||||
color: root.lock.pam.buffer ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface
|
color: Colours.palette.m3onSurfaceVariant
|
||||||
fontStyle: Tokens.font.icon.size(Tokens.font.icon.large.pointSize).weight(Font.Medium).build()
|
fontStyle: Tokens.font.icon.builders.medium.scale(root.centerScale * 1.2).build()
|
||||||
|
opacity: root.lock.pam.buffer ? 0 : 1
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
Anim {
|
||||||
|
type: Anim.DefaultEffects
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,7 @@ Item {
|
||||||
opacity: root.stateMsgShouldBeVisible && !root.msg ? 1 : 0
|
opacity: root.stateMsgShouldBeVisible && !root.msg ? 1 : 0
|
||||||
color: Colours.palette.m3onSurfaceVariant
|
color: Colours.palette.m3onSurfaceVariant
|
||||||
|
|
||||||
font: Tokens.font.mono.small
|
font: Tokens.font.body.small
|
||||||
horizontalAlignment: Qt.AlignHCenter
|
horizontalAlignment: Qt.AlignHCenter
|
||||||
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
|
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
|
||||||
lineHeight: 1.2
|
lineHeight: 1.2
|
||||||
|
|
@ -145,7 +145,7 @@ Item {
|
||||||
opacity: 0
|
opacity: 0
|
||||||
color: Colours.palette.m3error
|
color: Colours.palette.m3error
|
||||||
|
|
||||||
font: Tokens.font.mono.small
|
font: Tokens.font.body.small
|
||||||
horizontalAlignment: Qt.AlignHCenter
|
horizontalAlignment: Qt.AlignHCenter
|
||||||
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
|
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue