summaryrefslogtreecommitdiff
path: root/components/controls
diff options
context:
space:
mode:
Diffstat (limited to 'components/controls')
-rw-r--r--components/controls/CollapsibleSection.qml132
-rw-r--r--components/controls/CustomSpinBox.qml74
-rw-r--r--components/controls/FilledSlider.qml2
-rw-r--r--components/controls/IconTextButton.qml9
-rw-r--r--components/controls/SpinBoxRow.qml53
-rw-r--r--components/controls/StyledScrollBar.qml96
-rw-r--r--components/controls/StyledSlider.qml2
-rw-r--r--components/controls/StyledTextField.qml2
-rw-r--r--components/controls/SwitchRow.qml49
-rw-r--r--components/controls/ToggleButton.qml82
-rw-r--r--components/controls/ToggleRow.qml29
11 files changed, 511 insertions, 19 deletions
diff --git a/components/controls/CollapsibleSection.qml b/components/controls/CollapsibleSection.qml
new file mode 100644
index 0000000..078145b
--- /dev/null
+++ b/components/controls/CollapsibleSection.qml
@@ -0,0 +1,132 @@
+import ".."
+import qs.components
+import qs.components.effects
+import qs.services
+import qs.config
+import QtQuick
+import QtQuick.Layouts
+
+ColumnLayout {
+ id: root
+
+ required property string title
+ property string description: ""
+ property bool expanded: false
+ property bool showBackground: false
+
+ signal toggleRequested
+
+ spacing: Appearance.spacing.small
+ Layout.fillWidth: true
+
+ Item {
+ id: sectionHeaderItem
+ Layout.fillWidth: true
+ Layout.preferredHeight: Math.max(titleRow.implicitHeight + Appearance.padding.normal * 2, 48)
+
+ RowLayout {
+ id: titleRow
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.leftMargin: Appearance.padding.normal
+ anchors.rightMargin: Appearance.padding.normal
+ spacing: Appearance.spacing.normal
+
+ StyledText {
+ text: root.title
+ font.pointSize: Appearance.font.size.larger
+ font.weight: 500
+ }
+
+ Item {
+ Layout.fillWidth: true
+ }
+
+ MaterialIcon {
+ text: "expand_more"
+ rotation: root.expanded ? 180 : 0
+ color: Colours.palette.m3onSurfaceVariant
+ font.pointSize: Appearance.font.size.normal
+ Behavior on rotation {
+ Anim {
+ duration: Appearance.anim.durations.small
+ easing.bezierCurve: Appearance.anim.curves.standard
+ }
+ }
+ }
+ }
+
+ StateLayer {
+ anchors.fill: parent
+ color: Colours.palette.m3onSurface
+ radius: Appearance.rounding.normal
+ showHoverBackground: false
+ function onClicked(): void {
+ root.toggleRequested();
+ root.expanded = !root.expanded;
+ }
+ }
+ }
+
+ default property alias content: contentColumn.data
+
+ Item {
+ id: contentWrapper
+ Layout.fillWidth: true
+ Layout.preferredHeight: root.expanded ? (contentColumn.implicitHeight + Appearance.spacing.small * 2) : 0
+ clip: true
+
+ Behavior on Layout.preferredHeight {
+ Anim {
+ easing.bezierCurve: Appearance.anim.curves.standard
+ }
+ }
+
+ StyledRect {
+ id: backgroundRect
+ anchors.fill: parent
+ radius: Appearance.rounding.normal
+ color: Colours.layer(Colours.palette.m3surfaceContainer, 2)
+ opacity: root.showBackground && root.expanded ? 1.0 : 0.0
+ visible: root.showBackground
+
+ Behavior on opacity {
+ Anim {
+ easing.bezierCurve: Appearance.anim.curves.standard
+ }
+ }
+ }
+
+ ColumnLayout {
+ id: contentColumn
+ anchors.left: parent.left
+ anchors.right: parent.right
+ y: Appearance.spacing.small
+ anchors.leftMargin: Appearance.padding.normal
+ anchors.rightMargin: Appearance.padding.normal
+ anchors.bottomMargin: Appearance.spacing.small
+ spacing: Appearance.spacing.small
+ opacity: root.expanded ? 1.0 : 0.0
+
+ Behavior on opacity {
+ Anim {
+ easing.bezierCurve: Appearance.anim.curves.standard
+ }
+ }
+
+ StyledText {
+ id: descriptionText
+ Layout.fillWidth: true
+ Layout.topMargin: root.description !== "" ? Appearance.spacing.smaller : 0
+ Layout.bottomMargin: root.description !== "" ? Appearance.spacing.small : 0
+ visible: root.description !== ""
+ text: root.description
+ color: Colours.palette.m3onSurfaceVariant
+ font.pointSize: Appearance.font.size.small
+ wrapMode: Text.Wrap
+ }
+ }
+ }
+}
+
diff --git a/components/controls/CustomSpinBox.qml b/components/controls/CustomSpinBox.qml
index e2ed508..438dc08 100644
--- a/components/controls/CustomSpinBox.qml
+++ b/components/controls/CustomSpinBox.qml
@@ -9,19 +9,69 @@ import QtQuick.Layouts
RowLayout {
id: root
- property int value
+ property real value
property real max: Infinity
property real min: -Infinity
+ property real step: 1
property alias repeatRate: timer.interval
- signal valueModified(value: int)
+ signal valueModified(value: real)
spacing: Appearance.spacing.small
+ property bool isEditing: false
+ property string displayText: root.value.toString()
+
+ onValueChanged: {
+ if (!root.isEditing) {
+ root.displayText = root.value.toString();
+ }
+ }
+
StyledTextField {
+ id: textField
+
inputMethodHints: Qt.ImhFormattedNumbersOnly
- text: root.value
- onAccepted: root.valueModified(text)
+ text: root.isEditing ? text : root.displayText
+ validator: DoubleValidator {
+ bottom: root.min
+ top: root.max
+ decimals: root.step < 1 ? Math.max(1, Math.ceil(-Math.log10(root.step))) : 0
+ }
+ onActiveFocusChanged: {
+ if (activeFocus) {
+ root.isEditing = true;
+ } else {
+ root.isEditing = false;
+ root.displayText = root.value.toString();
+ }
+ }
+ 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;
+ }
+ 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;
+ }
padding: Appearance.padding.small
leftPadding: Appearance.padding.normal
@@ -50,7 +100,13 @@ RowLayout {
onReleased: timer.stop()
function onClicked(): void {
- root.valueModified(Math.min(root.max, root.value + 1));
+ 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);
}
}
@@ -79,7 +135,13 @@ RowLayout {
onReleased: timer.stop()
function onClicked(): void {
- root.valueModified(Math.max(root.min, root.value - 1));
+ 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);
}
}
diff --git a/components/controls/FilledSlider.qml b/components/controls/FilledSlider.qml
index 78b8a5c..80dd44c 100644
--- a/components/controls/FilledSlider.qml
+++ b/components/controls/FilledSlider.qml
@@ -15,7 +15,7 @@ Slider {
orientation: Qt.Vertical
background: StyledRect {
- color: Colours.tPalette.m3surfaceContainer
+ color: Colours.layer(Colours.palette.m3surfaceContainer, 2)
radius: Appearance.rounding.full
StyledRect {
diff --git a/components/controls/IconTextButton.qml b/components/controls/IconTextButton.qml
index 78e7c5b..0badd7a 100644
--- a/components/controls/IconTextButton.qml
+++ b/components/controls/IconTextButton.qml
@@ -2,6 +2,7 @@ import ".."
import qs.services
import qs.config
import QtQuick
+import QtQuick.Layouts
StyledRect {
id: root
@@ -53,7 +54,7 @@ StyledRect {
}
}
- Row {
+ RowLayout {
id: row
anchors.centerIn: parent
@@ -62,7 +63,8 @@ StyledRect {
MaterialIcon {
id: iconLabel
- anchors.verticalCenter: parent.verticalCenter
+ Layout.alignment: Qt.AlignVCenter
+ Layout.topMargin: Math.round(fontInfo.pointSize * 0.0575)
color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour
fill: root.internalChecked ? 1 : 0
@@ -74,7 +76,8 @@ StyledRect {
StyledText {
id: label
- anchors.verticalCenter: parent.verticalCenter
+ Layout.alignment: Qt.AlignVCenter
+ Layout.topMargin: -Math.round(iconLabel.fontInfo.pointSize * 0.0575)
color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour
}
}
diff --git a/components/controls/SpinBoxRow.qml b/components/controls/SpinBoxRow.qml
new file mode 100644
index 0000000..4902627
--- /dev/null
+++ b/components/controls/SpinBoxRow.qml
@@ -0,0 +1,53 @@
+import ".."
+import qs.components
+import qs.components.effects
+import qs.services
+import qs.config
+import QtQuick
+import QtQuick.Layouts
+
+StyledRect {
+ id: root
+
+ required property string label
+ required property real value
+ required property real min
+ required property real max
+ property real step: 1
+ property var onValueModified: function(value) {}
+
+ Layout.fillWidth: true
+ implicitHeight: row.implicitHeight + Appearance.padding.large * 2
+ radius: Appearance.rounding.normal
+ color: Colours.layer(Colours.palette.m3surfaceContainer, 2)
+
+ Behavior on implicitHeight {
+ Anim {}
+ }
+
+ RowLayout {
+ id: row
+
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.margins: Appearance.padding.large
+ spacing: Appearance.spacing.normal
+
+ StyledText {
+ Layout.fillWidth: true
+ text: root.label
+ }
+
+ CustomSpinBox {
+ min: root.min
+ max: root.max
+ step: root.step
+ value: root.value
+ onValueModified: value => {
+ root.onValueModified(value);
+ }
+ }
+ }
+}
+
diff --git a/components/controls/StyledScrollBar.qml b/components/controls/StyledScrollBar.qml
index fc641b5..de8b679 100644
--- a/components/controls/StyledScrollBar.qml
+++ b/components/controls/StyledScrollBar.qml
@@ -19,14 +19,51 @@ ScrollBar {
shouldBeActive = flickable.moving;
}
+ property bool _updatingFromFlickable: false
+ property bool _updatingFromUser: false
+
+ // Sync nonAnimPosition with Qt's automatic position binding
onPositionChanged: {
- if (position === nonAnimPosition)
+ if (_updatingFromUser) {
+ _updatingFromUser = false;
+ return;
+ }
+ if (position === nonAnimPosition) {
animating = false;
- else if (!animating)
+ return;
+ }
+ if (!animating && !_updatingFromFlickable && !fullMouse.pressed) {
nonAnimPosition = position;
+ }
}
- position: nonAnimPosition
+ // Sync nonAnimPosition with flickable when not animating
+ Connections {
+ target: flickable
+ function onContentYChanged() {
+ if (!animating && !fullMouse.pressed) {
+ _updatingFromFlickable = true;
+ const contentHeight = flickable.contentHeight;
+ const height = flickable.height;
+ if (contentHeight > height) {
+ nonAnimPosition = Math.max(0, Math.min(1, flickable.contentY / (contentHeight - height)));
+ } else {
+ nonAnimPosition = 0;
+ }
+ _updatingFromFlickable = false;
+ }
+ }
+ }
+
+ Component.onCompleted: {
+ if (flickable) {
+ const contentHeight = flickable.contentHeight;
+ const height = flickable.height;
+ if (contentHeight > height) {
+ nonAnimPosition = Math.max(0, Math.min(1, flickable.contentY / (contentHeight - height)));
+ }
+ }
+ }
implicitWidth: Appearance.padding.small
contentItem: StyledRect {
@@ -86,17 +123,62 @@ ScrollBar {
onPressed: event => {
root.animating = true;
- root.nonAnimPosition = Math.max(0, Math.min(1 - root.size, event.y / root.height - root.size / 2));
+ root._updatingFromUser = true;
+ const newPos = Math.max(0, Math.min(1 - root.size, event.y / root.height - root.size / 2));
+ root.nonAnimPosition = newPos;
+ // Update flickable position
+ // Map scrollbar position [0, 1-size] to contentY [0, maxContentY]
+ if (root.flickable) {
+ const contentHeight = root.flickable.contentHeight;
+ const height = root.flickable.height;
+ if (contentHeight > height) {
+ const maxContentY = contentHeight - height;
+ const maxPos = 1 - root.size;
+ const contentY = maxPos > 0 ? (newPos / maxPos) * maxContentY : 0;
+ root.flickable.contentY = Math.max(0, Math.min(maxContentY, contentY));
+ }
+ }
}
- onPositionChanged: event => root.nonAnimPosition = Math.max(0, Math.min(1 - root.size, event.y / root.height - root.size / 2))
+ onPositionChanged: event => {
+ root._updatingFromUser = true;
+ const newPos = Math.max(0, Math.min(1 - root.size, event.y / root.height - root.size / 2));
+ root.nonAnimPosition = newPos;
+ // Update flickable position
+ // Map scrollbar position [0, 1-size] to contentY [0, maxContentY]
+ if (root.flickable) {
+ const contentHeight = root.flickable.contentHeight;
+ const height = root.flickable.height;
+ if (contentHeight > height) {
+ const maxContentY = contentHeight - height;
+ const maxPos = 1 - root.size;
+ const contentY = maxPos > 0 ? (newPos / maxPos) * maxContentY : 0;
+ root.flickable.contentY = Math.max(0, Math.min(maxContentY, contentY));
+ }
+ }
+ }
function onWheel(event: WheelEvent): void {
root.animating = true;
+ root._updatingFromUser = true;
+ let newPos = root.nonAnimPosition;
if (event.angleDelta.y > 0)
- root.nonAnimPosition = Math.max(0, root.nonAnimPosition - 0.1);
+ newPos = Math.max(0, root.nonAnimPosition - 0.1);
else if (event.angleDelta.y < 0)
- root.nonAnimPosition = Math.min(1 - root.size, root.nonAnimPosition + 0.1);
+ newPos = Math.min(1 - root.size, root.nonAnimPosition + 0.1);
+ root.nonAnimPosition = newPos;
+ // Update flickable position
+ // Map scrollbar position [0, 1-size] to contentY [0, maxContentY]
+ if (root.flickable) {
+ const contentHeight = root.flickable.contentHeight;
+ const height = root.flickable.height;
+ if (contentHeight > height) {
+ const maxContentY = contentHeight - height;
+ const maxPos = 1 - root.size;
+ const contentY = maxPos > 0 ? (newPos / maxPos) * maxContentY : 0;
+ root.flickable.contentY = Math.max(0, Math.min(maxContentY, contentY));
+ }
+ }
}
}
diff --git a/components/controls/StyledSlider.qml b/components/controls/StyledSlider.qml
index 92c8aa8..0ef229d 100644
--- a/components/controls/StyledSlider.qml
+++ b/components/controls/StyledSlider.qml
@@ -32,7 +32,7 @@ Slider {
implicitWidth: parent.width - root.handle.x - root.handle.implicitWidth - root.implicitHeight / 6
- color: Colours.tPalette.m3surfaceContainer
+ color: Colours.palette.m3surfaceContainerHighest
radius: Appearance.rounding.full
topLeftRadius: root.implicitHeight / 15
bottomLeftRadius: root.implicitHeight / 15
diff --git a/components/controls/StyledTextField.qml b/components/controls/StyledTextField.qml
index 4db87e9..60bcff2 100644
--- a/components/controls/StyledTextField.qml
+++ b/components/controls/StyledTextField.qml
@@ -13,7 +13,7 @@ TextField {
placeholderTextColor: Colours.palette.m3outline
font.family: Appearance.font.family.sans
font.pointSize: Appearance.font.size.smaller
- renderType: TextField.NativeRendering
+ renderType: echoMode === TextField.Password ? TextField.QtRendering : TextField.NativeRendering
cursorVisible: !readOnly
background: null
diff --git a/components/controls/SwitchRow.qml b/components/controls/SwitchRow.qml
new file mode 100644
index 0000000..7fa3e1b
--- /dev/null
+++ b/components/controls/SwitchRow.qml
@@ -0,0 +1,49 @@
+import ".."
+import qs.components
+import qs.components.effects
+import qs.services
+import qs.config
+import QtQuick
+import QtQuick.Layouts
+
+StyledRect {
+ id: root
+
+ required property string label
+ required property bool checked
+ property bool enabled: true
+ property var onToggled: function(checked) {}
+
+ Layout.fillWidth: true
+ implicitHeight: row.implicitHeight + Appearance.padding.large * 2
+ radius: Appearance.rounding.normal
+ color: Colours.layer(Colours.palette.m3surfaceContainer, 2)
+
+ Behavior on implicitHeight {
+ Anim {}
+ }
+
+ RowLayout {
+ id: row
+
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.margins: Appearance.padding.large
+ spacing: Appearance.spacing.normal
+
+ StyledText {
+ Layout.fillWidth: true
+ text: root.label
+ }
+
+ StyledSwitch {
+ checked: root.checked
+ enabled: root.enabled
+ onToggled: {
+ root.onToggled(checked);
+ }
+ }
+ }
+}
+
diff --git a/components/controls/ToggleButton.qml b/components/controls/ToggleButton.qml
new file mode 100644
index 0000000..9d8e094
--- /dev/null
+++ b/components/controls/ToggleButton.qml
@@ -0,0 +1,82 @@
+import ".."
+import qs.components
+import qs.components.effects
+import qs.services
+import qs.config
+import QtQuick
+import QtQuick.Layouts
+
+StyledRect {
+ id: root
+
+ required property bool toggled
+ property string icon
+ property string label
+ property string accent: "Secondary"
+
+ signal clicked
+
+ Layout.preferredWidth: implicitWidth + (toggleStateLayer.pressed ? Appearance.padding.normal * 2 : toggled ? Appearance.padding.small * 2 : 0)
+ implicitWidth: toggleBtnInner.implicitWidth + Appearance.padding.large * 2
+ implicitHeight: toggleBtnIcon.implicitHeight + Appearance.padding.normal * 2
+
+ radius: toggled || toggleStateLayer.pressed ? Appearance.rounding.small : Math.min(width, height) / 2 * Math.min(1, Appearance.rounding.scale)
+ color: toggled ? Colours.palette[`m3${accent.toLowerCase()}`] : Colours.palette[`m3${accent.toLowerCase()}Container`]
+
+ StateLayer {
+ id: toggleStateLayer
+
+ color: root.toggled ? Colours.palette[`m3on${root.accent}`] : Colours.palette[`m3on${root.accent}Container`]
+
+ function onClicked(): void {
+ root.clicked();
+ }
+ }
+
+ RowLayout {
+ id: toggleBtnInner
+
+ anchors.centerIn: parent
+ spacing: Appearance.spacing.normal
+
+ MaterialIcon {
+ id: toggleBtnIcon
+
+ visible: !!text
+ fill: root.toggled ? 1 : 0
+ text: root.icon
+ color: root.toggled ? Colours.palette[`m3on${root.accent}`] : Colours.palette[`m3on${root.accent}Container`]
+ font.pointSize: Appearance.font.size.large
+
+ Behavior on fill {
+ Anim {}
+ }
+ }
+
+ Loader {
+ asynchronous: true
+ active: !!root.label
+ visible: active
+
+ sourceComponent: StyledText {
+ text: root.label
+ color: root.toggled ? Colours.palette[`m3on${root.accent}`] : Colours.palette[`m3on${root.accent}Container`]
+ }
+ }
+ }
+
+ Behavior on radius {
+ Anim {
+ duration: Appearance.anim.durations.expressiveFastSpatial
+ easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
+ }
+ }
+
+ Behavior on Layout.preferredWidth {
+ Anim {
+ duration: Appearance.anim.durations.expressiveFastSpatial
+ easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
+ }
+ }
+}
+
diff --git a/components/controls/ToggleRow.qml b/components/controls/ToggleRow.qml
new file mode 100644
index 0000000..23dc2a2
--- /dev/null
+++ b/components/controls/ToggleRow.qml
@@ -0,0 +1,29 @@
+import qs.components
+import qs.components.controls
+import qs.services
+import qs.config
+import QtQuick
+import QtQuick.Layouts
+
+RowLayout {
+ id: root
+
+ required property string label
+ property alias checked: toggle.checked
+ property alias toggle: toggle
+
+ Layout.fillWidth: true
+ spacing: Appearance.spacing.normal
+
+ StyledText {
+ Layout.fillWidth: true
+ text: root.label
+ }
+
+ StyledSwitch {
+ id: toggle
+
+ cLayer: 2
+ }
+}
+