diff options
Diffstat (limited to 'components/controls')
| -rw-r--r-- | components/controls/CustomMouseArea.qml | 21 | ||||
| -rw-r--r-- | components/controls/CustomSpinBox.qml | 108 | ||||
| -rw-r--r-- | components/controls/StyledBusyIndicator.qml | 90 | ||||
| -rw-r--r-- | components/controls/StyledScrollBar.qml | 36 | ||||
| -rw-r--r-- | components/controls/StyledSwitch.qml | 160 | ||||
| -rw-r--r-- | components/controls/StyledTextField.qml | 86 | ||||
| -rw-r--r-- | components/controls/VerticalSlider.qml | 137 |
7 files changed, 638 insertions, 0 deletions
diff --git a/components/controls/CustomMouseArea.qml b/components/controls/CustomMouseArea.qml new file mode 100644 index 0000000..7c973c2 --- /dev/null +++ b/components/controls/CustomMouseArea.qml @@ -0,0 +1,21 @@ +import QtQuick + +MouseArea { + property int scrollAccumulatedY: 0 + + function onWheel(event: WheelEvent): void { + } + + onWheel: event => { + // Update accumulated scroll + if (Math.sign(event.angleDelta.y) !== Math.sign(scrollAccumulatedY)) + scrollAccumulatedY = 0; + scrollAccumulatedY += event.angleDelta.y; + + // Trigger handler and reset if above threshold + if (Math.abs(scrollAccumulatedY) >= 120) { + onWheel(event); + scrollAccumulatedY = 0; + } + } +} diff --git a/components/controls/CustomSpinBox.qml b/components/controls/CustomSpinBox.qml new file mode 100644 index 0000000..4611bed --- /dev/null +++ b/components/controls/CustomSpinBox.qml @@ -0,0 +1,108 @@ +pragma ComponentBehavior: Bound + +import ".." +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +RowLayout { + id: root + + property int value + property real max: Infinity + property real min: -Infinity + property alias repeatRate: timer.interval + + signal valueModified(value: int) + + spacing: Appearance.spacing.small + + StyledTextField { + inputMethodHints: Qt.ImhFormattedNumbersOnly + text: root.value + onAccepted: root.valueModified(text) + + padding: Appearance.padding.small + leftPadding: Appearance.padding.normal + rightPadding: Appearance.padding.normal + + background: StyledRect { + implicitWidth: 100 + radius: Appearance.rounding.small + color: Colours.palette.m3surfaceContainerHigh + } + } + + StyledRect { + radius: Appearance.rounding.small + color: Colours.palette.m3primary + + implicitWidth: implicitHeight + implicitHeight: upIcon.implicitHeight + Appearance.padding.small * 2 + + StateLayer { + id: upState + + color: Colours.palette.m3onPrimary + + onPressAndHold: timer.start() + onReleased: timer.stop() + + function onClicked(): void { + root.valueModified(Math.min(root.max, root.value + 1)); + } + } + + MaterialIcon { + id: upIcon + + anchors.centerIn: parent + text: "keyboard_arrow_up" + color: Colours.palette.m3onPrimary + } + } + + StyledRect { + radius: Appearance.rounding.small + color: Colours.palette.m3primary + + implicitWidth: implicitHeight + implicitHeight: downIcon.implicitHeight + Appearance.padding.small * 2 + + StateLayer { + id: downState + + color: Colours.palette.m3onPrimary + + onPressAndHold: timer.start() + onReleased: timer.stop() + + function onClicked(): void { + root.valueModified(Math.max(root.min, root.value - 1)); + } + } + + MaterialIcon { + id: downIcon + + anchors.centerIn: parent + text: "keyboard_arrow_down" + color: Colours.palette.m3onPrimary + } + } + + Timer { + id: timer + + interval: 100 + repeat: true + triggeredOnStart: true + onTriggered: { + if (upState.pressed) + upState.onClicked(); + else if (downState.pressed) + downState.onClicked(); + } + } +} diff --git a/components/controls/StyledBusyIndicator.qml b/components/controls/StyledBusyIndicator.qml new file mode 100644 index 0000000..060870f --- /dev/null +++ b/components/controls/StyledBusyIndicator.qml @@ -0,0 +1,90 @@ +pragma ComponentBehavior: Bound + +import qs.services +import qs.config +import QtQuick +import QtQuick.Controls +import QtQuick.Shapes + +BusyIndicator { + id: root + + property color fgColour: Colours.palette.m3onPrimaryContainer + property color bgColour: Colours.palette.m3primaryContainer + + background: null + + contentItem: Shape { + id: shape + + preferredRendererType: Shape.CurveRenderer + asynchronous: true + + RotationAnimator on rotation { + from: 0 + to: 180 + running: root.visible && root.running + loops: Animation.Infinite + duration: Appearance.anim.durations.extraLarge + easing.type: Easing.Linear + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } + + ShapePath { + strokeWidth: Math.min(root.implicitWidth, root.implicitHeight) * 0.18 + strokeColor: root.bgColour + fillColor: "transparent" + capStyle: ShapePath.RoundCap + + PathAngleArc { + centerX: shape.width / 2 + centerY: shape.height / 2 + radiusX: root.implicitWidth / 2 + radiusY: root.implicitHeight / 2 + startAngle: 0 + sweepAngle: 360 + } + + Behavior on strokeColor { + ColorAnimation { + duration: Appearance.anim.durations.normal + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.standard + } + } + } + + ShapePath { + strokeWidth: Math.min(root.implicitWidth, root.implicitHeight) * 0.18 + strokeColor: root.fgColour + fillColor: "transparent" + capStyle: ShapePath.RoundCap + + PathAngleArc { + centerX: shape.width / 2 + centerY: shape.height / 2 + radiusX: root.implicitWidth / 2 + radiusY: root.implicitHeight / 2 + startAngle: -sweepAngle / 2 + sweepAngle: 60 + } + + PathAngleArc { + centerX: shape.width / 2 + centerY: shape.height / 2 + radiusX: root.implicitWidth / 2 + radiusY: root.implicitHeight / 2 + startAngle: 180 - sweepAngle / 2 + sweepAngle: 60 + } + + Behavior on strokeColor { + ColorAnimation { + duration: Appearance.anim.durations.normal + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.standard + } + } + } + } +} diff --git a/components/controls/StyledScrollBar.qml b/components/controls/StyledScrollBar.qml new file mode 100644 index 0000000..61ddc6d --- /dev/null +++ b/components/controls/StyledScrollBar.qml @@ -0,0 +1,36 @@ +import ".." +import qs.services +import qs.config +import QtQuick +import QtQuick.Controls + +ScrollBar { + id: root + + contentItem: StyledRect { + implicitWidth: 6 + opacity: root.pressed ? 1 : root.policy === ScrollBar.AlwaysOn || (root.active && root.size < 1) ? 0.8 : 0 + radius: Appearance.rounding.full + color: Colours.palette.m3secondary + + Behavior on opacity { + NumberAnimation { + duration: Appearance.anim.durations.normal + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.standard + } + } + } + + CustomMouseArea { + z: -1 + anchors.fill: parent + + function onWheel(event: WheelEvent): void { + if (event.angleDelta.y > 0) + root.decrease(); + else if (event.angleDelta.y < 0) + root.increase(); + } + } +} diff --git a/components/controls/StyledSwitch.qml b/components/controls/StyledSwitch.qml new file mode 100644 index 0000000..c9d7330 --- /dev/null +++ b/components/controls/StyledSwitch.qml @@ -0,0 +1,160 @@ +import ".." +import qs.services +import qs.config +import QtQuick +import QtQuick.Controls +import QtQuick.Shapes + +Switch { + id: root + + implicitWidth: implicitIndicatorWidth + implicitHeight: implicitIndicatorHeight + + indicator: StyledRect { + radius: Appearance.rounding.full + color: root.checked ? Colours.palette.m3primary : Colours.palette.m3surfaceContainerHighest + + implicitWidth: implicitHeight * 1.7 + implicitHeight: Appearance.font.size.normal + Appearance.padding.smaller * 2 + + StyledRect { + readonly property real nonAnimWidth: root.pressed ? implicitHeight * 1.3 : implicitHeight + + radius: Appearance.rounding.full + color: root.checked ? Colours.palette.m3onPrimary : Colours.palette.m3outline + + x: root.checked ? parent.implicitWidth - nonAnimWidth - Appearance.padding.small / 2 : Appearance.padding.small / 2 + implicitWidth: nonAnimWidth + implicitHeight: parent.implicitHeight - Appearance.padding.small + anchors.verticalCenter: parent.verticalCenter + + StyledRect { + anchors.fill: parent + radius: parent.radius + + color: root.checked ? Colours.palette.m3primary : Colours.palette.m3onSurface + opacity: root.pressed ? 0.1 : root.hovered ? 0.08 : 0 + + Behavior on opacity { + NumberAnim {} + } + } + + Shape { + id: icon + + property point start1: { + if (root.pressed) + return Qt.point(width * 0.2, height / 2); + if (root.checked) + return Qt.point(width * 0.15, height / 2); + return Qt.point(width * 0.15, height * 0.15); + } + property point end1: { + if (root.pressed) { + if (root.checked) + return Qt.point(width * 0.4, height / 2); + return Qt.point(width * 0.8, height / 2); + } + if (root.checked) + return Qt.point(width * 0.4, height * 0.7); + return Qt.point(width * 0.85, height * 0.85); + } + property point start2: { + if (root.pressed) { + if (root.checked) + return Qt.point(width * 0.4, height / 2); + return Qt.point(width * 0.2, height / 2); + } + if (root.checked) + return Qt.point(width * 0.4, height * 0.7); + return Qt.point(width * 0.15, height * 0.85); + } + property point end2: { + if (root.pressed) + return Qt.point(width * 0.8, height / 2); + if (root.checked) + return Qt.point(width * 0.85, height * 0.2); + return Qt.point(width * 0.85, height * 0.15); + } + + anchors.centerIn: parent + width: height + height: parent.implicitHeight - Appearance.padding.small * 2 + preferredRendererType: Shape.CurveRenderer + asynchronous: true + + ShapePath { + strokeWidth: Appearance.font.size.larger * 0.15 + strokeColor: root.checked ? Colours.palette.m3primary : Colours.palette.m3surfaceContainerHighest + fillColor: "transparent" + capStyle: ShapePath.RoundCap + + startX: icon.start1.x + startY: icon.start1.y + + PathLine { + x: icon.end1.x + y: icon.end1.y + } + PathMove { + x: icon.start2.x + y: icon.start2.y + } + PathLine { + x: icon.end2.x + y: icon.end2.y + } + + Behavior on strokeColor { + ColorAnimation { + duration: Appearance.anim.durations.normal + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.standard + } + } + } + + Behavior on start1 { + PropAnim {} + } + Behavior on end1 { + PropAnim {} + } + Behavior on start2 { + PropAnim {} + } + Behavior on end2 { + PropAnim {} + } + } + + Behavior on x { + NumberAnim {} + } + + Behavior on implicitWidth { + NumberAnim {} + } + } + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + enabled: false + } + + component NumberAnim: NumberAnimation { + duration: Appearance.anim.durations.normal + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.standard + } + + component PropAnim: PropertyAnimation { + duration: Appearance.anim.durations.normal + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.standard + } +} diff --git a/components/controls/StyledTextField.qml b/components/controls/StyledTextField.qml new file mode 100644 index 0000000..30db314 --- /dev/null +++ b/components/controls/StyledTextField.qml @@ -0,0 +1,86 @@ +pragma ComponentBehavior: Bound + +import ".." +import qs.services +import qs.config +import QtQuick +import QtQuick.Controls + +TextField { + id: root + + color: Colours.palette.m3onSurface + placeholderTextColor: Colours.palette.m3outline + font.family: Appearance.font.family.sans + font.pointSize: Appearance.font.size.smaller + renderType: TextField.NativeRendering + cursorVisible: !readOnly + + background: null + + cursorDelegate: StyledRect { + id: cursor + + property bool disableBlink + + implicitWidth: 2 + color: Colours.palette.m3primary + radius: Appearance.rounding.normal + + Connections { + target: root + + function onCursorPositionChanged(): void { + if (root.activeFocus && root.cursorVisible) { + cursor.opacity = 1; + cursor.disableBlink = true; + enableBlink.restart(); + } + } + } + + Timer { + id: enableBlink + + interval: 100 + onTriggered: cursor.disableBlink = false + } + + Timer { + running: root.activeFocus && root.cursorVisible && !cursor.disableBlink + repeat: true + triggeredOnStart: true + interval: 500 + onTriggered: parent.opacity = parent.opacity === 1 ? 0 : 1 + } + + Binding { + when: !root.activeFocus || !root.cursorVisible + cursor.opacity: 0 + } + + Behavior on opacity { + NumberAnimation { + duration: Appearance.anim.durations.small + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.standard + } + } + } + + Behavior on color { + ColorAnimation { + duration: Appearance.anim.durations.normal + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.standard + } + } + + Behavior on placeholderTextColor { + ColorAnimation { + duration: Appearance.anim.durations.normal + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.standard + } + } +} diff --git a/components/controls/VerticalSlider.qml b/components/controls/VerticalSlider.qml new file mode 100644 index 0000000..306cc24 --- /dev/null +++ b/components/controls/VerticalSlider.qml @@ -0,0 +1,137 @@ +import ".." +import "../effects" +import qs.services +import qs.config +import QtQuick +import QtQuick.Controls + +Slider { + id: root + + required property string icon + property real oldValue + + orientation: Qt.Vertical + + background: StyledRect { + color: Colours.alpha(Colours.palette.m3surfaceContainer, true) + radius: Appearance.rounding.full + + StyledRect { + anchors.left: parent.left + anchors.right: parent.right + + y: root.handle.y + implicitHeight: parent.height - y + + color: Colours.alpha(Colours.palette.m3secondary, true) + radius: Appearance.rounding.full + } + } + + handle: Item { + id: handle + + property bool moving + + y: root.visualPosition * (root.availableHeight - height) + implicitWidth: root.width + implicitHeight: root.width + + Elevation { + anchors.fill: parent + radius: rect.radius + level: handleInteraction.containsMouse ? 2 : 1 + } + + StyledRect { + id: rect + + anchors.fill: parent + + color: Colours.alpha(Colours.palette.m3inverseSurface, true) + radius: Appearance.rounding.full + + MouseArea { + id: handleInteraction + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.NoButton + } + + MaterialIcon { + id: icon + + property bool moving: handle.moving + + function update(): void { + animate = !moving; + text = moving ? Qt.binding(() => Math.round(root.value * 100)) : Qt.binding(() => root.icon); + font.pointSize = moving ? Appearance.font.size.small : Appearance.font.size.larger; + font.family = moving ? Appearance.font.family.sans : Appearance.font.family.material; + } + + animate: true + text: root.icon + color: Colours.palette.m3inverseOnSurface + anchors.centerIn: parent + + Behavior on moving { + SequentialAnimation { + NumberAnimation { + target: icon + property: "scale" + from: 1 + to: 0 + duration: Appearance.anim.durations.normal / 2 + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.standardAccel + } + ScriptAction { + script: icon.update() + } + NumberAnimation { + target: icon + property: "scale" + from: 0 + to: 1 + duration: Appearance.anim.durations.normal / 2 + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.standardDecel + } + } + } + } + } + } + + onPressedChanged: handle.moving = pressed + + onValueChanged: { + if (Math.abs(value - oldValue) < 0.01) + return; + oldValue = value; + handle.moving = true; + stateChangeDelay.restart(); + } + + Timer { + id: stateChangeDelay + + interval: 500 + onTriggered: { + if (!root.pressed) + handle.moving = false; + } + } + + Behavior on value { + NumberAnimation { + duration: Appearance.anim.durations.large + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.standard + } + } +} |