1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-08-02 03:28:28 -04:00
Files
DankMaterialShell/quickshell/Widgets/DankButtonGroup.qml
T
2026-07-04 10:44:02 -04:00

272 lines
10 KiB
QML

import QtQuick
import Quickshell
import qs.Common
import qs.Widgets
Row {
id: root
function checkParentDisablesTransparency() {
let p = parent;
while (p) {
if (p.disablePopupTransparency === true)
return true;
p = p.parent;
}
return false;
}
property var model: []
property int currentIndex: -1
property string selectionMode: "single"
property bool multiSelect: selectionMode === "multi"
property var initialSelection: []
property var currentSelection: initialSelection
property bool checkEnabled: true
property string size: "medium"
property int buttonHeight: size === "small" ? 32 : 40
property int minButtonWidth: size === "small" ? 56 : 64
property int buttonPadding: size === "small" ? Theme.spacingM : Theme.spacingL
property int checkIconSize: size === "small" ? Theme.iconSizeSmall - 2 : Theme.iconSizeSmall
property int textSize: size === "small" ? Theme.fontSizeSmall : Theme.fontSizeMedium
property bool userInteracted: false
property bool usePopupTransparency: !checkParentDisablesTransparency()
property real maximumWidth: -1
readonly property real _segmentCap: {
const count = model?.length ?? 0;
if (maximumWidth <= 0 || count === 0)
return -1;
return (maximumWidth - spacing * (count - 1)) / count - 4;
}
signal selectionChanged(int index, bool selected)
signal animationCompleted
spacing: Theme.spacingXS
Timer {
id: animationTimer
interval: Theme.shortDuration
onTriggered: {
root.userInteracted = false;
root.animationCompleted();
}
}
function isSelected(index) {
if (multiSelect) {
return repeater.itemAt(index)?.selected || false;
}
return index === currentIndex;
}
function selectItem(index) {
userInteracted = true;
if (multiSelect) {
const modelValue = model[index];
let newSelection = [...currentSelection];
const isCurrentlySelected = newSelection.includes(modelValue);
if (isCurrentlySelected) {
newSelection = newSelection.filter(item => item !== modelValue);
} else {
newSelection.push(modelValue);
}
currentSelection = newSelection;
selectionChanged(index, !isCurrentlySelected);
animationTimer.restart();
} else {
const oldIndex = currentIndex;
selectionChanged(index, true);
if (oldIndex !== index && oldIndex >= 0) {
selectionChanged(oldIndex, false);
}
animationTimer.restart();
}
}
Repeater {
id: repeater
model: ScriptModel {
values: root.model
}
delegate: Rectangle {
id: segment
property bool selected: multiSelect ? root.currentSelection.includes(modelData) : (index === root.currentIndex)
property bool hovered: mouseArea.containsMouse
property bool pressed: mouseArea.pressed
property bool isFirst: index === 0
property bool isLast: index === repeater.count - 1
property bool visualFirst: I18n.isRtl ? isLast : isFirst
property bool visualLast: I18n.isRtl ? isFirst : isLast
property bool prevSelected: index > 0 ? root.isSelected(index - 1) : false
property bool nextSelected: index < repeater.count - 1 ? root.isSelected(index + 1) : false
readonly property real contentNaturalWidth: (checkIcon.visible ? checkIcon.width + contentRow.spacing : 0) + buttonText.implicitWidth
width: {
const natural = Math.max(contentNaturalWidth + root.buttonPadding * 2, root.minButtonWidth);
const capped = root._segmentCap > 0 ? Math.min(natural, Math.max(root._segmentCap, root.minButtonWidth)) : natural;
return capped + (selected ? 4 : 0);
}
height: root.buttonHeight
color: selected ? Theme.buttonBg : (root.usePopupTransparency ? Theme.withAlpha(Theme.surfaceVariant, Theme.popupTransparency) : Theme.surfaceVariant)
border.color: "transparent"
border.width: 0
topLeftRadius: (visualFirst || selected) ? Theme.cornerRadius : Math.min(4, Theme.cornerRadius)
bottomLeftRadius: (visualFirst || selected) ? Theme.cornerRadius : Math.min(4, Theme.cornerRadius)
topRightRadius: (visualLast || selected) ? Theme.cornerRadius : Math.min(4, Theme.cornerRadius)
bottomRightRadius: (visualLast || selected) ? Theme.cornerRadius : Math.min(4, Theme.cornerRadius)
Behavior on width {
enabled: root.userInteracted
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on topLeftRadius {
enabled: root.userInteracted
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on topRightRadius {
enabled: root.userInteracted
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on bottomLeftRadius {
enabled: root.userInteracted
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on bottomRightRadius {
enabled: root.userInteracted
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on color {
enabled: root.userInteracted
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Rectangle {
id: stateLayer
anchors.fill: parent
topLeftRadius: parent.topLeftRadius
bottomLeftRadius: parent.bottomLeftRadius
topRightRadius: parent.topRightRadius
bottomRightRadius: parent.bottomRightRadius
color: {
if (pressed)
return selected ? Theme.buttonPressed : Theme.surfaceTextHover;
if (hovered)
return selected ? Theme.buttonHover : Theme.surfaceTextHover;
return "transparent";
}
Behavior on color {
ColorAnimation {
duration: Theme.shorterDuration
easing.type: Theme.standardEasing
}
}
}
DankRipple {
id: segmentRipple
cornerRadius: Theme.cornerRadius
rippleColor: segment.selected ? Theme.buttonText : Theme.surfaceVariantText
}
Item {
id: contentItem
anchors.centerIn: parent
implicitWidth: contentRow.implicitWidth
implicitHeight: contentRow.implicitHeight
Row {
id: contentRow
spacing: Theme.spacingS
DankIcon {
id: checkIcon
name: "check"
size: root.checkIconSize
color: segment.selected ? Theme.buttonText : Theme.surfaceVariantText
visible: root.checkEnabled && segment.selected
opacity: segment.selected ? 1 : 0
scale: segment.selected ? 1 : 0.6
anchors.verticalCenter: parent.verticalCenter
Behavior on opacity {
enabled: root.userInteracted
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on scale {
enabled: root.userInteracted
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
}
StyledText {
id: buttonText
readonly property real capAvailable: {
if (root._segmentCap <= 0)
return -1;
const cap = Math.max(root._segmentCap, root.minButtonWidth);
return Math.max(0, cap - root.buttonPadding * 2 - (checkIcon.visible ? checkIcon.width + contentRow.spacing : 0));
}
text: typeof modelData === "string" ? modelData : modelData.text || ""
font.pixelSize: root.textSize
font.weight: segment.selected ? Font.Medium : Font.Normal
color: segment.selected ? Theme.buttonText : Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
width: capAvailable < 0 ? implicitWidth : Math.min(implicitWidth, capAvailable)
maximumLineCount: 1
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPressed: mouse => segmentRipple.trigger(mouse.x, mouse.y)
onClicked: root.selectItem(index)
}
}
}
}