new spin box, textfield and searchbar
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 10s
Python / lint-format (pull_request) Successful in 30s
Python / test (pull_request) Successful in 43s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m38s
C++ / build (pull_request) Successful in 2m18s

This commit is contained in:
2026-06-30 13:25:12 +02:00
parent 437f935f73
commit 157bdd8fd9
19 changed files with 641 additions and 1055 deletions
+94 -142
View File
@@ -1,180 +1,132 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import QtQuick.Templates
import qs.Config
RowLayout {
DoubleSpinBox {
id: root
property string displayText: root.value.toString()
property bool isEditing: false
property real max: Infinity
property real min: -Infinity
property alias repeatRate: timer.interval
property real step: 1
property real value
property int cLayer: 1
property int repeatDecay: 50
property int repeatRate: 400
signal valueModified(value: real)
function decrease(): void {
let newValue = Math.max(from, value - stepSize);
const decimals = stepSize < 1 ? Math.max(1, Math.ceil(-Math.log10(stepSize))) : 0;
newValue = Math.round(newValue * Math.pow(10, decimals)) / Math.pow(10, decimals);
value = newValue;
valueModified();
}
function increase(): void {
let newValue = Math.min(to, value + stepSize);
const decimals = stepSize < 1 ? Math.max(1, Math.ceil(-Math.log10(stepSize))) : 0;
newValue = Math.round(newValue * Math.pow(10, decimals)) / Math.pow(10, decimals);
value = newValue;
valueModified();
}
decimals: stepSize < 1 ? Math.max(1, Math.ceil(-Math.log10(stepSize))) : 0
editable: true
implicitHeight: Math.max(up.indicator.implicitHeight, down.indicator.implicitHeight, contentItem.implicitHeight) + topPadding + bottomPadding
implicitWidth: contentItem.implicitWidth + leftPadding + rightPadding
leftPadding: up.indicator.implicitWidth + Appearance.spacing.extraSmall / 2
rightPadding: down.indicator.implicitWidth + Appearance.spacing.extraSmall / 2
spacing: Appearance.spacing.small
onValueChanged: {
if (!root.isEditing) {
root.displayText = root.value.toString();
}
}
CustomTextField {
id: textField
color: root.enabled ? DynamicColors.palette.m3onSurface : Qt.alpha(DynamicColors.palette.m3onSurface, 0.5)
implicitHeight: upButton.implicitHeight
contentItem: TextFieldBase {
horizontalAlignment: TextField.AlignHCenter
implicitWidth: 65
inputMethodHints: Qt.ImhFormattedNumbersOnly
leftPadding: Appearance.padding.normal
padding: Appearance.padding.small
rightPadding: Appearance.padding.normal
text: root.isEditing ? text : root.displayText
leftPadding: Appearance.padding.larger
readOnly: !root.editable
rightPadding: Appearance.padding.larger
text: root.textFromValue(root.value, root.locale)
validator: root.validator
background: CustomRect {
color: root.enabled ? DynamicColors.tPalette.m3surfaceContainerHigh : DynamicColors.tPalette.m3surfaceContainerLow
implicitWidth: 100
radius: Appearance.rounding.full
}
validator: DoubleValidator {
bottom: root.min
decimals: root.step < 1 ? Math.max(1, Math.ceil(-Math.log10(root.step))) : 0
top: root.max
}
onAccepted: {
const numValue = parseFloat(text);
if (!isNaN(numValue)) {
const clampedValue = Math.max(root.min, Math.min(root.max, numValue));
root.value = clampedValue;
root.displayText = clampedValue.toString();
root.valueModified(clampedValue);
} else {
text = root.displayText;
}
root.isEditing = false;
}
onActiveFocusChanged: {
if (activeFocus) {
root.isEditing = true;
} else {
root.isEditing = false;
root.displayText = root.value.toString();
}
}
onEditingFinished: {
if (text !== root.displayText) {
const numValue = parseFloat(text);
if (!isNaN(numValue)) {
const clampedValue = Math.max(root.min, Math.min(root.max, numValue));
root.value = clampedValue;
root.displayText = clampedValue.toString();
root.valueModified(clampedValue);
} else {
text = root.displayText;
}
}
root.isEditing = false;
color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, root.cLayer)
radius: Appearance.rounding.extraSmall
}
}
down.indicator: IconButton {
id: downButton
CustomRect {
bottomRightRadius: pressed ? Appearance.rounding.small : Appearance.rounding.extraSmall
color: enabled ? DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, root.cLayer) : disabledColor
disabledColor: Qt.alpha(DynamicColors.palette.m3surfaceContainerHighest, 0.4)
icon: "remove"
isRound: true
label.anchors.horizontalCenterOffset: pressed ? 0 : 2
padding: Appearance.padding.extraSmall
topRightRadius: pressed ? Appearance.rounding.small : Appearance.rounding.extraSmall
type: IconButton.Text
Behavior on bottomRightRadius {
Anim {
type: Anim.DefaultEffects
}
}
Behavior on label.anchors.horizontalCenterOffset {
Anim {
type: Anim.DefaultEffects
}
}
Behavior on topRightRadius {
Anim {
type: Anim.DefaultEffects
}
}
}
up.indicator: IconButton {
id: upButton
color: root.enabled ? DynamicColors.palette.m3primary : DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, 1)
implicitHeight: upIcon.implicitHeight + Appearance.padding.extraSmall * 2
implicitWidth: implicitHeight
radius: upState.pressed ? Appearance.rounding.small : Appearance.rounding.normal
anchors.right: parent.right
bottomLeftRadius: pressed ? Appearance.rounding.small : Appearance.rounding.extraSmall
color: enabled ? DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, root.cLayer) : disabledColor
disabledColor: Qt.alpha(DynamicColors.palette.m3surfaceContainerHighest, 0.4)
icon: "add"
isRound: true
label.anchors.horizontalCenterOffset: pressed ? 0 : -2
padding: Appearance.padding.extraSmall
topLeftRadius: pressed ? Appearance.rounding.small : Appearance.rounding.extraSmall
type: IconButton.Text
Behavior on radius {
Behavior on bottomLeftRadius {
Anim {
type: Anim.FastEffects
type: Anim.DefaultEffects
}
}
StateLayer {
id: upState
color: DynamicColors.palette.m3onPrimary
onClicked: {
let newValue = Math.min(root.max, root.value + root.step);
// Round to avoid floating point precision errors
const decimals = root.step < 1 ? Math.max(1, Math.ceil(-Math.log10(root.step))) : 0;
newValue = Math.round(newValue * Math.pow(10, decimals)) / Math.pow(10, decimals);
root.value = newValue;
root.displayText = newValue.toString();
root.valueModified(newValue);
}
onPressAndHold: timer.start()
onReleased: timer.stop()
}
MaterialIcon {
id: upIcon
anchors.centerIn: parent
color: root.enabled ? DynamicColors.palette.m3onPrimary : Qt.alpha(DynamicColors.palette.m3onSurface, 0.5)
text: "keyboard_arrow_up"
}
}
CustomRect {
color: root.enabled ? DynamicColors.palette.m3primary : DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, 1)
implicitHeight: downIcon.implicitHeight + Appearance.padding.extraSmall * 2
implicitWidth: implicitHeight
radius: downState.pressed ? Appearance.rounding.small : Appearance.rounding.normal
Behavior on radius {
Behavior on label.anchors.horizontalCenterOffset {
Anim {
type: Anim.FastEffects
type: Anim.DefaultEffects
}
}
StateLayer {
id: downState
color: DynamicColors.palette.m3onPrimary
onClicked: {
let newValue = Math.max(root.min, root.value - root.step);
// Round to avoid floating point precision errors
const decimals = root.step < 1 ? Math.max(1, Math.ceil(-Math.log10(root.step))) : 0;
newValue = Math.round(newValue * Math.pow(10, decimals)) / Math.pow(10, decimals);
root.value = newValue;
root.displayText = newValue.toString();
root.valueModified(newValue);
Behavior on topLeftRadius {
Anim {
type: Anim.DefaultEffects
}
onPressAndHold: timer.start()
onReleased: timer.stop()
}
MaterialIcon {
id: downIcon
anchors.centerIn: parent
color: root.enabled ? DynamicColors.palette.m3onPrimary : Qt.alpha(DynamicColors.palette.m3onSurface, 0.5)
text: "keyboard_arrow_down"
}
}
Timer {
id: timer
interval: 100
interval: root.repeatRate
repeat: true
running: upButton.pressed || downButton.pressed
triggeredOnStart: true
onRunningChanged: {
if (!running)
interval = root.repeatRate;
}
onTriggered: {
if (upState.pressed)
upState.onClicked();
else if (downState.pressed)
downState.onClicked();
if (upButton.pressed)
root.increase();
else if (downButton.pressed)
root.decrease();
if (interval > root.repeatDecay)
interval -= root.repeatDecay;
}
}
}
+267 -49
View File
@@ -1,77 +1,295 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import QtQuick.Shapes
import qs.Config
TextField {
TextFieldBase {
id: root
property int cursorHeight: root.height
enum TextFieldType {
Outlined,
Filled
}
background: null
color: DynamicColors.palette.m3onSurface
cursorVisible: !readOnly
font.family: Appearance.font.family.sans
font.pointSize: Appearance.font.size.smaller
placeholderTextColor: DynamicColors.palette.m3outline
renderType: echoMode === TextField.Password ? TextField.QtRendering : TextField.NativeRendering
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 ? Appearance.spacing.small : 0
readonly property int horizontalPadding: Appearance.padding.large
property bool isError
property string leadingIcon
readonly property int leadingOffset: leadingIcon ? leadingIconLoader.width + leadingIconLoader.anchors.leftMargin : 0
readonly property real outlineGap: placeholder.width * root.smallFontScale + Appearance.spacing.extraSmall * 2
property real outlineGapScale: activeFocus || text ? 1 : 0
property int radius: Appearance.rounding.small
readonly property real smallFontScale: smallFontSize / font.pointSize
property int smallFontSize: Appearance.font.size.small
property string supportingText
readonly property int supportingTextOffset: effectiveSupportingText ? supportingTextLoader.height + Appearance.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
Behavior on color {
CAnim {
bottomPadding: Appearance.padding.large + supportingTextOffset - filledOffset
leftPadding: horizontalPadding + leadingOffset
rightPadding: horizontalPadding + trailingOffset
topPadding: Appearance.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
}
}
cursorDelegate: CustomRect {
id: cursor
property bool disableBlink
color: DynamicColors.palette.m3primary
implicitWidth: 2
radius: Appearance.rounding.normal
Behavior on opacity {
Anim {
duration: Appearance.anim.durations.small
}
Behavior on outlineGapScale {
Anim {
type: Anim.DefaultEffects
}
}
Connections {
function onCursorPositionChanged(): void {
if (root.activeFocus && root.cursorVisible) {
cursor.opacity = 1;
cursor.disableBlink = true;
enableBlink.restart();
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: Appearance.padding.extraSmall
anchors.verticalCenter: parent.verticalCenter
color: root.isError ? DynamicColors.palette.m3error : (root.activeFocus ? DynamicColors.palette.m3primary : root.text ? DynamicColors.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 + -Appearance.spacing.extraSmall
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: Appearance.anim.durations.expressiveEffects
easing: Appearance.anim.curves.expressiveDefaultEffects
}
}
target: root
}
Timer {
id: enableBlink
Loader {
id: leadingIconLoader
interval: 100
active: root.leadingIcon
anchors.left: parent.left
anchors.leftMargin: Appearance.padding.larger
anchors.verticalCenter: parent.verticalCenter
onTriggered: cursor.disableBlink = false
sourceComponent: MaterialIcon {
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.large
text: root.leadingIcon
}
}
Timer {
interval: 500
repeat: true
running: root.activeFocus && root.cursorVisible && !cursor.disableBlink
triggeredOnStart: true
Loader {
id: trailingIconLoader
onTriggered: parent.opacity = parent.opacity === 1 ? 0 : 1
}
active: root.trailingIcon
anchors.right: parent.right
anchors.rightMargin: Appearance.padding.larger
anchors.verticalCenter: parent.verticalCenter
Binding {
cursor.opacity: 0
when: !root.activeFocus || !root.cursorVisible
sourceComponent: MaterialIcon {
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.large
text: root.trailingIcon
}
}
}
Behavior on placeholderTextColor {
CAnim {
Loader {
id: supportingTextLoader
active: root.effectiveSupportingText
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.leftMargin: root.horizontalPadding
sourceComponent: CustomText {
color: root.isError ? DynamicColors.palette.m3error : DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.small
text: root.effectiveSupportingText
}
}
TapHandler {
id: tapHandler
}
Component {
id: outlineComp
Shape {
id: bg
asynchronous: true
preferredRendererType: Shape.CurveRenderer
ShapePath {
capStyle: ShapePath.RoundCap
fillColor: "transparent"
startX: root.horizontalPadding - root.clampedRadius + root.outlineGap * (1 - root.outlineGapScale) / 2 + root.outlineGap * root.outlineGapScale
strokeColor: root.isError ? DynamicColors.palette.m3error : (root.activeFocus ? DynamicColors.palette.m3primary : DynamicColors.palette.m3outline)
strokeWidth: root.activeFocus ? 2 : 1
Behavior on strokeColor {
CAnim {
}
}
Behavior on strokeWidth {
Anim {
}
}
PathLine {
x: bg.width - root.clampedRadius
}
PathArc {
radiusX: root.clampedRadius
radiusY: root.clampedRadius
x: bg.width
y: root.clampedRadius
}
PathLine {
x: bg.width
y: bg.height - root.clampedRadius
}
PathArc {
radiusX: root.clampedRadius
radiusY: root.clampedRadius
x: bg.width - root.clampedRadius
y: bg.height
}
PathLine {
x: root.clampedRadius
y: bg.height
}
PathArc {
radiusX: root.clampedRadius
radiusY: root.clampedRadius
x: 0
y: bg.height - root.clampedRadius
}
PathLine {
x: 0
y: root.clampedRadius
}
PathArc {
radiusX: root.clampedRadius
radiusY: root.clampedRadius
x: root.clampedRadius
y: 0
}
PathLine {
x: root.horizontalPadding - root.clampedRadius + root.outlineGap * (1 - root.outlineGapScale) / 2
}
}
}
}
Component {
id: filledComp
CustomRect {
color: root.activeFocus ? DynamicColors.tPalette.m3surfaceContainerHighest : DynamicColors.tPalette.m3surfaceContainerHigh
topLeftRadius: root.clampedRadius
topRightRadius: root.clampedRadius
CustomRect {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
color: root.isError ? DynamicColors.palette.m3error : (root.activeFocus ? DynamicColors.palette.m3primary : DynamicColors.palette.m3outline)
implicitHeight: root.activeFocus ? 2 : 1
Behavior on implicitHeight {
Anim {
}
}
}
}
}
}
+2 -1
View File
@@ -30,8 +30,9 @@ MouseArea {
signal itemSelected(item: MenuItem)
anchors.fill: parent
cursorShape: undefined
cursorShape: expanded ? Qt.ArrowCursor : undefined
enabled: expanded
hoverEnabled: expanded
layer.enabled: opacity < 1
opacity: expanded ? 1 : 0
parent: {
+94
View File
@@ -0,0 +1,94 @@
import QtQuick
import qs.Config
TextFieldBase {
id: root
readonly property alias bg: bg
readonly property alias clearIcon: clearIcon
readonly property alias searchIcon: searchIcon
bottomPadding: Appearance.padding.large
leftPadding: searchIcon.width + searchIcon.anchors.leftMargin + Appearance.spacing.small
rightPadding: clearIcon.width + clearIcon.anchors.rightMargin + Appearance.spacing.small
topPadding: Appearance.padding.large
background: CustomRect {
id: bg
anchors.fill: parent
color: DynamicColors.tPalette.m3surfaceContainer
radius: Appearance.rounding.full
StateLayer {
id: stateLayer
cursorShape: Qt.IBeamCursor
enabled: !root.activeFocus
manualPressOverride: tapHandler.pressed
onClicked: root.focus = true
}
}
onPressed: {
if (stateLayer.enabled)
stateLayer.press(stateLayer.mouseX, stateLayer.mouseY);
}
CustomText {
id: placeholder
anchors.left: parent.left
anchors.leftMargin: root.leftPadding
anchors.verticalCenter: parent.verticalCenter
color: root.placeholderTextColor
font: root.font
opacity: root.text ? 0 : 1
text: root.placeholderText
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
}
MaterialIcon {
id: searchIcon
anchors.left: parent.left
anchors.leftMargin: Appearance.padding.large
anchors.verticalCenter: parent.verticalCenter
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.large
text: "search"
}
IconButton {
id: clearIcon
anchors.right: parent.right
anchors.rightMargin: Appearance.padding.larger
anchors.verticalCenter: parent.verticalCenter
enabled: root.text
icon: "clear"
opacity: root.text ? 1 : 0
radius: Appearance.rounding.full
radiusMorph: false
stateLayer.hoverEnabled: enabled
type: IconButton.Text
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
onClicked: root.clear()
}
TapHandler {
id: tapHandler
}
}
+88
View File
@@ -0,0 +1,88 @@
import QtQuick
import QtQuick.Templates
import qs.Config
TextField {
id: root
color: DynamicColors.palette.m3onSurface
cursorVisible: !readOnly
font.pointSize: Appearance.font.size.small
implicitHeight: contentHeight + topPadding + bottomPadding
implicitWidth: contentWidth + leftPadding + rightPadding
placeholderTextColor: DynamicColors.palette.m3onSurfaceVariant // No anim cause placeholder is custom
renderType: echoMode === TextField.Password ? TextField.QtRendering : TextField.NativeRendering
selectedTextColor: color
selectionColor: Qt.alpha(DynamicColors.palette.m3primary, 0.4)
verticalAlignment: TextInput.AlignVCenter
Behavior on color {
CAnim {
}
}
cursorDelegate: Item {
}
Behavior on selectionColor {
CAnim {
}
}
CustomRect {
id: cursor
property bool disableBlink
color: DynamicColors.palette.m3primary
implicitHeight: root.cursorRectangle.height
implicitWidth: 1.5
radius: Appearance.rounding.large
x: root.cursorRectangle.x
y: root.cursorRectangle.y
Behavior on opacity {
Anim {
type: Anim.StandardSmall
}
}
Behavior on x {
Anim {
duration: Appearance.anim.durations.expressiveFastEffects
easing.bezierCurve: [0.2, 1, 0.21, 1, 1, 1]
}
}
Connections {
function onCursorPositionChanged(): void {
if (root.activeFocus && root.cursorVisible) {
cursor.opacity = 1;
cursor.disableBlink = true;
enableBlink.restart();
}
}
target: root
}
Timer {
id: enableBlink
interval: 500
onTriggered: cursor.disableBlink = false
}
Timer {
interval: 500
repeat: true
running: root.activeFocus && root.cursorVisible && !cursor.disableBlink
triggeredOnStart: true
onTriggered: parent.opacity = parent.opacity === 1 ? 0 : 1
}
Binding {
cursor.opacity: 0
when: !root.activeFocus || !root.cursorVisible
}
}
}