302 lines
7.9 KiB
QML
302 lines
7.9 KiB
QML
pragma ComponentBehavior: Bound
|
|
|
|
import QtQuick
|
|
import QtQuick.Shapes
|
|
import ZShell.Config
|
|
import qs.Services
|
|
|
|
TextFieldBase {
|
|
id: root
|
|
|
|
enum TextFieldType {
|
|
Outlined,
|
|
Filled
|
|
}
|
|
|
|
readonly property int clampedRadius: Math.min(horizontalPadding, Math.min(width, height) / 2, radius)
|
|
readonly property string effectiveSupportingText: isError && errorText ? errorText : supportingText
|
|
property bool emptyIsValid: true
|
|
property string errorText
|
|
readonly property int filledOffset: type === CustomTextField.Filled ? Tokens.spacing.small : 0
|
|
readonly property int horizontalPadding: Tokens.padding.large
|
|
property bool isError
|
|
property string leadingIcon
|
|
readonly property int leadingOffset: leadingIcon ? leadingIconLoader.width + leadingIconLoader.anchors.leftMargin : 0
|
|
property int radius: Tokens.rounding.small
|
|
readonly property real smallFontScale: smallFontSize / font.pointSize
|
|
property int smallFontSize: Tokens.font.size.small
|
|
property string supportingText
|
|
readonly property int supportingTextOffset: effectiveSupportingText ? supportingTextLoader.height + Tokens.spacing.extraSmall : 0
|
|
property string trailingIcon
|
|
readonly property int trailingOffset: trailingIcon ? trailingIconLoader.width + trailingIconLoader.anchors.rightMargin : 0
|
|
property int type: CustomTextField.Outlined
|
|
readonly property bool valid: !validate || (!text && emptyIsValid) || (validate instanceof RegExp ? validate.test(text) : !!validate(text))
|
|
property var validate // Regex or function
|
|
|
|
bottomPadding: Tokens.padding.large + supportingTextOffset - filledOffset
|
|
leftPadding: horizontalPadding + leadingOffset
|
|
rightPadding: horizontalPadding + trailingOffset
|
|
topPadding: Tokens.padding.large + filledOffset
|
|
|
|
background: Loader {
|
|
anchors.bottomMargin: root.supportingTextOffset
|
|
anchors.fill: parent
|
|
sourceComponent: root.type === CustomTextField.Filled ? filledComp : outlineComp
|
|
|
|
StateLayer {
|
|
id: stateLayer
|
|
|
|
cursorShape: Qt.IBeamCursor
|
|
enabled: !root.activeFocus
|
|
manualPressOverride: tapHandler.pressed
|
|
radius: root.type === CustomTextField.Outlined ? root.clampedRadius : 0
|
|
topLeftRadius: root.clampedRadius
|
|
topRightRadius: root.clampedRadius
|
|
|
|
onClicked: root.focus = true
|
|
}
|
|
}
|
|
|
|
onEditingFinished: {
|
|
if (!valid)
|
|
isError = true;
|
|
}
|
|
onPressed: {
|
|
if (stateLayer.enabled)
|
|
stateLayer.press(stateLayer.mouseX, stateLayer.mouseY);
|
|
}
|
|
onTextEdited: {
|
|
if (isError)
|
|
isError = false;
|
|
}
|
|
|
|
Item {
|
|
id: contentWrapper
|
|
|
|
anchors.bottomMargin: root.supportingTextOffset
|
|
anchors.fill: parent
|
|
|
|
CustomText {
|
|
id: placeholder
|
|
|
|
anchors.left: parent.left
|
|
anchors.leftMargin: root.leftPadding
|
|
anchors.topMargin: Tokens.padding.extraSmall
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
color: root.isError ? Colors.palette.m3error : (root.activeFocus ? Colors.palette.m3primary : root.text ? Colors.palette.m3outline : root.placeholderTextColor)
|
|
font.family: root.font.family
|
|
font.pointSize: root.font.pointSize
|
|
font.variableAxes: root.font.variableAxes
|
|
font.weight: root.font.weight
|
|
renderType: Text.QtRendering
|
|
text: root.placeholderText
|
|
|
|
states: [
|
|
State {
|
|
name: "smallOutlined"
|
|
when: root.type === CustomTextField.Outlined && (root.activeFocus || root.text)
|
|
|
|
PropertyChanges {
|
|
placeholder.anchors.leftMargin: -(1 - root.smallFontScale) * placeholder.width / 2 + root.horizontalPadding
|
|
placeholder.scale: root.smallFontScale
|
|
}
|
|
|
|
AnchorChanges {
|
|
anchors.verticalCenter: contentWrapper.top
|
|
target: placeholder
|
|
}
|
|
},
|
|
State {
|
|
name: "smallFilled"
|
|
when: root.type === CustomTextField.Filled && (root.activeFocus || root.text)
|
|
|
|
PropertyChanges {
|
|
placeholder.anchors.leftMargin: -(1 - root.smallFontScale) * placeholder.width / 2 + root.horizontalPadding + root.leadingOffset
|
|
placeholder.scale: root.smallFontScale
|
|
}
|
|
|
|
AnchorChanges {
|
|
anchors.top: contentWrapper.top
|
|
anchors.verticalCenter: undefined
|
|
target: placeholder
|
|
}
|
|
}
|
|
]
|
|
transitions: Transition {
|
|
Anim {
|
|
properties: "scale,leftMargin"
|
|
type: Anim.DefaultEffects
|
|
}
|
|
|
|
AnchorAnim {
|
|
duration: Tokens.anim.durations.expressiveEffects
|
|
easing.bezierCurve: Tokens.anim.curves.expressiveEffects
|
|
}
|
|
}
|
|
}
|
|
|
|
Loader {
|
|
id: leadingIconLoader
|
|
|
|
active: root.leadingIcon
|
|
anchors.left: parent.left
|
|
anchors.leftMargin: Tokens.padding.normal
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
|
|
sourceComponent: MaterialIcon {
|
|
color: Colors.palette.m3onSurfaceVariant
|
|
text: root.leadingIcon
|
|
}
|
|
}
|
|
|
|
Loader {
|
|
id: trailingIconLoader
|
|
|
|
active: root.trailingIcon
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: Tokens.padding.normal
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
|
|
sourceComponent: MaterialIcon {
|
|
color: Colors.palette.m3onSurfaceVariant
|
|
text: root.trailingIcon
|
|
}
|
|
}
|
|
}
|
|
|
|
Loader {
|
|
id: supportingTextLoader
|
|
|
|
active: root.effectiveSupportingText
|
|
anchors.bottom: parent.bottom
|
|
anchors.left: parent.left
|
|
anchors.leftMargin: root.horizontalPadding
|
|
|
|
sourceComponent: CustomText {
|
|
color: root.isError ? Colors.palette.m3error : Colors.palette.m3onSurfaceVariant
|
|
font.pointSize: Tokens.font.size.small
|
|
text: root.effectiveSupportingText
|
|
}
|
|
}
|
|
|
|
TapHandler {
|
|
id: tapHandler
|
|
}
|
|
|
|
Component {
|
|
id: outlineComp
|
|
|
|
Shape {
|
|
id: bg
|
|
|
|
asynchronous: true
|
|
preferredRendererType: Shape.CurveRenderer
|
|
|
|
ShapePath {
|
|
id: path
|
|
|
|
readonly property real inset: strokeWidth / 2
|
|
readonly property real outlineGap: placeholder.width * root.smallFontScale + Tokens.spacing.small * 2
|
|
property real outlineGapScale: root.activeFocus || root.text ? 1 : 0
|
|
|
|
capStyle: ShapePath.RoundCap
|
|
fillColor: "transparent"
|
|
startX: path.inset + root.horizontalPadding - root.clampedRadius + path.outlineGap * (1 - path.outlineGapScale) / 2 + path.outlineGap * path.outlineGapScale
|
|
startY: path.inset
|
|
strokeColor: root.isError ? Colors.palette.m3error : (root.activeFocus ? Colors.palette.m3primary : Colors.palette.m3outline)
|
|
strokeWidth: root.activeFocus ? 2 : 1
|
|
|
|
Behavior on outlineGapScale {
|
|
Anim {
|
|
type: Anim.DefaultEffects
|
|
}
|
|
}
|
|
Behavior on strokeColor {
|
|
CAnim {
|
|
}
|
|
}
|
|
Behavior on strokeWidth {
|
|
Anim {
|
|
}
|
|
}
|
|
|
|
PathLine {
|
|
x: bg.width - path.inset - root.clampedRadius
|
|
y: path.inset
|
|
}
|
|
|
|
PathArc {
|
|
radiusX: root.clampedRadius
|
|
radiusY: root.clampedRadius
|
|
x: bg.width - path.inset
|
|
y: path.inset + root.clampedRadius
|
|
}
|
|
|
|
PathLine {
|
|
x: bg.width - path.inset
|
|
y: bg.height - path.inset - root.clampedRadius
|
|
}
|
|
|
|
PathArc {
|
|
radiusX: root.clampedRadius
|
|
radiusY: root.clampedRadius
|
|
x: bg.width - path.inset - root.clampedRadius
|
|
y: bg.height - path.inset
|
|
}
|
|
|
|
PathLine {
|
|
x: path.inset + root.clampedRadius
|
|
y: bg.height - path.inset
|
|
}
|
|
|
|
PathArc {
|
|
radiusX: root.clampedRadius
|
|
radiusY: root.clampedRadius
|
|
x: path.inset
|
|
y: bg.height - path.inset - root.clampedRadius
|
|
}
|
|
|
|
PathLine {
|
|
x: path.inset
|
|
y: path.inset + root.clampedRadius
|
|
}
|
|
|
|
PathArc {
|
|
radiusX: root.clampedRadius
|
|
radiusY: root.clampedRadius
|
|
x: path.inset + root.clampedRadius
|
|
y: path.inset
|
|
}
|
|
|
|
PathLine {
|
|
x: path.inset + root.horizontalPadding - root.clampedRadius / 2 + path.outlineGap * (1 - path.outlineGapScale) / 2
|
|
y: path.inset
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Component {
|
|
id: filledComp
|
|
|
|
CustomRect {
|
|
color: root.activeFocus ? Colors.tPalette.m3surfaceContainerHighest : Colors.tPalette.m3surfaceContainerHigh
|
|
topLeftRadius: root.clampedRadius
|
|
topRightRadius: root.clampedRadius
|
|
|
|
CustomRect {
|
|
anchors.bottom: parent.bottom
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
color: root.isError ? Colors.palette.m3error : (root.activeFocus ? Colors.palette.m3primary : Colors.palette.m3outline)
|
|
implicitHeight: root.activeFocus ? 2 : 1
|
|
|
|
Behavior on implicitHeight {
|
|
Anim {
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|