summaryrefslogtreecommitdiff
path: root/components/controls
diff options
context:
space:
mode:
Diffstat (limited to 'components/controls')
-rw-r--r--components/controls/CustomMouseArea.qml21
-rw-r--r--components/controls/CustomSpinBox.qml108
-rw-r--r--components/controls/StyledBusyIndicator.qml90
-rw-r--r--components/controls/StyledScrollBar.qml36
-rw-r--r--components/controls/StyledSwitch.qml160
-rw-r--r--components/controls/StyledTextField.qml86
-rw-r--r--components/controls/VerticalSlider.qml137
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
+ }
+ }
+}