From c45c04e9ac7c17237c7a2202103798931f61aa71 Mon Sep 17 00:00:00 2001 From: kossLAN Date: Tue, 17 Jun 2025 10:23:00 -0400 Subject: [PATCH] save progress --- README.md | 11 +- bar/Bar.qml | 12 +- bar/battery/BatteryIndicator.qml | 123 ------------------- bar/mpris/Button.qml | 57 +++------ bar/power/BatteryIndicator.qml | 102 ++++++++++++++++ bar/systray/SysTray.qml | 67 ++++------ bar/volume/ApplicationMixer.qml | 70 +++++++++++ bar/volume/DeviceMixer.qml | 64 ++++++++++ bar/volume/VolumeCard.qml | 50 ++++++++ bar/volume/VolumeControl.qml | 76 ++++++++++++ bar/volume/VolumeIndicator.qml | 27 ++++ widgets/FontIconButton.qml | 48 ++++++++ widgets/MaterialButton.qml | 23 +++- widgets/MaterialSlider.qml | 105 ++++++++++++++++ widgets/TextButton.qml | 204 ------------------------------- 15 files changed, 621 insertions(+), 418 deletions(-) delete mode 100644 bar/battery/BatteryIndicator.qml create mode 100644 bar/power/BatteryIndicator.qml create mode 100644 bar/volume/ApplicationMixer.qml create mode 100644 bar/volume/DeviceMixer.qml create mode 100644 bar/volume/VolumeCard.qml create mode 100644 bar/volume/VolumeControl.qml create mode 100644 bar/volume/VolumeIndicator.qml create mode 100644 widgets/FontIconButton.qml create mode 100644 widgets/MaterialSlider.qml delete mode 100644 widgets/TextButton.qml diff --git a/README.md b/README.md index 6c61743..1b30359 100644 --- a/README.md +++ b/README.md @@ -4,16 +4,15 @@ The idea is to eventually be minimal but also use material 3 design language, lo ## TODO List - [x] Custom Popup Window Surface for smooth anims on top bar -- [ ] Lockscreen -- [ ] Port visuals to more material 3 aethestic -- [ ] Screenshot tool +- [ ] Lockscreen (WIP) +- [ ] Port visuals to more material 3 aethestic (WIP) +- [x] Screenshot tool (WIP - kinda scuffed, but is functional) - [ ] Recording/Clip widget with gpuscreenrecorder - [ ] Session Manager - [ ] Battery Profile Popup -- [ ] REDO Volume OSD +- [ ] REDO Volume OSD (WIP) - [ ] REDO Launcher (wallpaper picker, calculator, commands, etc...) -- [ ] Integrate wallpaper picker into launcher - [ ] Music Player Popup V4 lol ### Optionals -- [ ] Add toggle for floating or screen corners \ No newline at end of file +- [ ] Add toggle for floating or screen corners diff --git a/bar/Bar.qml b/bar/Bar.qml index bfde55d..92ac9f8 100644 --- a/bar/Bar.qml +++ b/bar/Bar.qml @@ -2,7 +2,8 @@ import QtQuick import QtQuick.Layouts import Quickshell import Quickshell.Widgets -import "battery" +import "power" +import "volume" import "systray" as SysTray import "popups" as Popup import "mpris" as Mpris @@ -99,6 +100,15 @@ PanelWindow { Layout.fillHeight: true } + VolumeIndicator { + id: volumeIndicator + popup: root.popup + Layout.preferredWidth: this.height + Layout.fillHeight: true + Layout.topMargin: 2 + Layout.bottomMargin: 2 + } + BatteryIndicator { id: batteryIndicator popup: root.popup diff --git a/bar/battery/BatteryIndicator.qml b/bar/battery/BatteryIndicator.qml deleted file mode 100644 index 0798823..0000000 --- a/bar/battery/BatteryIndicator.qml +++ /dev/null @@ -1,123 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import Qt5Compat.GraphicalEffects -import Quickshell.Widgets -import Quickshell.Services.UPower -import "../.." - -Item { - id: root - implicitWidth: height + 8 // for margin - visible: UPower.displayDevice.isLaptopBattery - - required property var popup - - MouseArea { - id: batteryButton - hoverEnabled: true - anchors.fill: parent - onClicked: { - if (root.popup.content == powerMenu) { - root.popup.hide(); - return; - } - - root.popup.set(this, powerMenu); - root.popup.show(); - } - } - - Item { - id: powerMenu - visible: false - implicitWidth: 250 - implicitHeight: 80 - - MouseArea { - anchors.fill: parent - onClicked: { - console.log("why this work"); - powerMenu.implicitWidth = 300; - } - } - - RowLayout { - anchors.fill: parent - - // placeholder for now - - // Text { - // text - // } - - ComboBox { - model: ["Power Save", "Balanced", "Performance"] - currentIndex: PowerProfiles.profile - onCurrentIndexChanged: PowerProfiles.profile = currentIndex - } - } - } - - Rectangle { - id: highlight - color: batteryButton.containsMouse ? ShellSettings.colors["primary"] : "transparent" - // radius: width / 2 - radius: 10 - - anchors { - fill: parent - // topMargin: 2 - // bottomMargin: 2 - } - - Behavior on color { - ColorAnimation { - duration: 100 - } - } - } - - Item { - implicitWidth: parent.height - implicitHeight: parent.height - anchors.centerIn: parent - layer.enabled: true - layer.effect: OpacityMask { - source: Rectangle { - width: root.width - height: root.height - color: "white" - } - - maskSource: IconImage { - implicitSize: root.width - source: "root:resources/battery/battery.svg" - } - } - - Rectangle { - id: batteryBackground - color: Qt.color(ShellSettings.colors["surface"]).lighter(4) - opacity: 0.75 - anchors { - fill: parent - margins: 2 - } - } - - Rectangle { - id: batteryPercentage - width: (parent.width - 4) * UPower.displayDevice.percentage - color: ShellSettings.colors["inverse_surface"] - - anchors { - left: batteryBackground.left - top: batteryBackground.top - bottom: batteryBackground.bottom - } - } - } -} diff --git a/bar/mpris/Button.qml b/bar/mpris/Button.qml index b719923..715aed8 100644 --- a/bar/mpris/Button.qml +++ b/bar/mpris/Button.qml @@ -1,14 +1,16 @@ -import Quickshell.Widgets import QtQuick import "../../mpris" as Mpris +import "../../widgets" as Widgets import "../.." -WrapperRectangle { +Widgets.MaterialButton { id: root - color: button.containsMouse ? ShellSettings.colors["primary"] : "transparent" radius: 6 - leftMargin: 5 - rightMargin: 5 + implicitWidth: mediaInfo.contentWidth + 8 + implicitHeight: parent.height + // onClicked: { + // popup.visible = !popup.visible; + // } required property var bar property var player: Mpris.Controller.trackedPlayer @@ -16,48 +18,25 @@ WrapperRectangle { Text { id: mediaInfo text: root.player?.trackTitle ?? "" - color: button.containsMouse ? ShellSettings.colors["inverse_primary"] : ShellSettings.colors["inverse_surface"] + color: root.containsMouse ? ShellSettings.colors["inverse_primary"] : ShellSettings.colors["inverse_surface"] elide: Text.ElideRight horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter font.pointSize: 11 - anchors.verticalCenter: parent.verticalCenter - - MouseArea { - id: button - anchors.fill: parent - hoverEnabled: true - - onClicked: { - popup.visible = !popup.visible; - } - } - - WidgetWindow { - id: popup - visible: false - parentWindow: root.bar - - // anchor.window: root.bar - } - - // Item { - // id: menu - // visible: false - // implicitWidth: 250 - // implicitHeight: 80 - // } + anchors.centerIn: parent Behavior on color { ColorAnimation { - duration: 100 + duration: 200 } } } - - Behavior on color { - ColorAnimation { - duration: 100 - } - } } + +// WidgetWindow { +// id: popup +// visible: false +// parentWindow: root.bar +// +// // anchor.window: root.bar +// } diff --git a/bar/power/BatteryIndicator.qml b/bar/power/BatteryIndicator.qml new file mode 100644 index 0000000..41681d9 --- /dev/null +++ b/bar/power/BatteryIndicator.qml @@ -0,0 +1,102 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Qt5Compat.GraphicalEffects +import Quickshell.Widgets +import Quickshell.Services.UPower +import "../../widgets" as Widgets +import "../.." + +// todo: redo the tray icon handling +Item { + id: root + implicitWidth: height + 8 // for margin + visible: UPower.displayDevice.isLaptopBattery + + required property var popup + + Widgets.MaterialButton { + id: batteryButton + hoverEnabled: true + onClicked: { + if (root.popup.content == powerMenu) { + root.popup.hide(); + return; + } + + root.popup.set(this, powerMenu); + root.popup.show(); + } + + anchors { + fill: parent + margins: 1 + } + + Item { + implicitWidth: parent.height + implicitHeight: parent.height + anchors.centerIn: parent + layer.enabled: true + layer.effect: OpacityMask { + source: Rectangle { + width: root.width + height: root.height + color: "white" + } + + maskSource: IconImage { + implicitSize: root.width + source: "root:resources/battery/battery.svg" + } + } + + Rectangle { + id: batteryBackground + color: Qt.color(ShellSettings.colors["surface"]).lighter(4) + opacity: 0.75 + anchors { + fill: parent + margins: 2 + } + } + + Rectangle { + id: batteryPercentage + width: (parent.width - 4) * UPower.displayDevice.percentage + color: ShellSettings.colors["inverse_surface"] + + anchors { + left: batteryBackground.left + top: batteryBackground.top + bottom: batteryBackground.bottom + } + } + } + } + + Item { + id: powerMenu + visible: false + implicitWidth: 250 + implicitHeight: 80 + + RowLayout { + anchors.fill: parent + + // ComboBox { + // model: ScriptModel { + // values: ["Power Save", "Balanced", "Performance"] + // } + // + // currentIndex: PowerProfiles.profile + // onCurrentIndexChanged: { + // PowerProfiles.profile = this.currentIndex; + // console.log(PowerProfile.toString(PowerProfiles.profile)); + // } + // } + } + } +} diff --git a/bar/systray/SysTray.qml b/bar/systray/SysTray.qml index 740e2e9..b572e3b 100644 --- a/bar/systray/SysTray.qml +++ b/bar/systray/SysTray.qml @@ -5,11 +5,11 @@ import QtQuick.Layouts import Quickshell import Quickshell.Widgets import Quickshell.Services.SystemTray -import "../.." +import "../../widgets" as Widgets RowLayout { id: root - spacing: 10 + spacing: 5 visible: SystemTray.items.values.length > 0 required property var popup @@ -19,14 +19,13 @@ RowLayout { delegate: Item { id: trayField - Layout.preferredWidth: parent.height + Layout.preferredWidth: parent.height Layout.fillHeight: true required property SystemTrayItem modelData - MouseArea { + Widgets.MaterialButton { id: trayButton hoverEnabled: true - anchors.fill: parent onClicked: { menuOpener.menu = trayField.modelData.menu; @@ -38,6 +37,25 @@ RowLayout { root.popup.set(this, trayMenu); root.popup.show(); } + + anchors { + fill: parent + margins: 2 + } + + IconImage { + id: trayIcon + anchors.fill: parent + source: { + // console.log(trayField.modelData.id); + switch (trayField.modelData.id) { + case "obs": + return "image://icon/obs-tray"; + default: + return trayField.modelData.icon; + } + } + } } QsMenuOpener { @@ -73,45 +91,6 @@ RowLayout { } } } - - Rectangle { - id: trayContainer - color: trayButton.containsMouse ? ShellSettings.colors["primary"] : "transparent" - radius: width / 2 - implicitHeight: parent.height - implicitWidth: parent.height - - anchors { - fill: parent - margins: 1 - } - - IconImage { - id: trayIcon - - source: { - // console.log(trayField.modelData.id); - - switch (trayField.modelData.id) { - case "obs": - return "image://icon/obs-tray"; - default: - return trayField.modelData.icon; - } - } - - anchors { - fill: parent - margins: 1 - } - } - - Behavior on color { - ColorAnimation { - duration: 100 - } - } - } } } } diff --git a/bar/volume/ApplicationMixer.qml b/bar/volume/ApplicationMixer.qml new file mode 100644 index 0000000..584be77 --- /dev/null +++ b/bar/volume/ApplicationMixer.qml @@ -0,0 +1,70 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import Quickshell.Widgets +import Quickshell.Services.Pipewire +import "../../widgets/" as Widgets +import "../.." + +ColumnLayout { + id: root + + Loader { + id: sinkLoader + active: sink + + property PwNode sink: Pipewire.defaultAudioSink + + sourceComponent: WrapperItem { + PwNodeLinkTracker { + id: linkTracker + node: sinkLoader.sink + } + + ColumnLayout { + Repeater { + model: linkTracker.linkGroups + + delegate: Loader { + id: nodeLoader + active: modelData.source !== null + Layout.preferredWidth: 350 + Layout.preferredHeight: 45 + + required property PwLinkGroup modelData + + sourceComponent: VolumeCard { + id: nodeCard + node: nodeLoader.modelData.source + text: node.properties["media.name"] ?? "" + + // if icon-name is undefined, just gonna fallback on the application name + icon: IconImage { + source: { + if (nodeCard.node.properties["application.icon-name"] !== undefined) + return `image://icon/${nodeCard.node.properties["application.icon-name"]}`; + + let applicationName = nodeCard.node.properties["application.name"]; + return `image://icon/${applicationName?.toLowerCase() ?? "image-missing"}`; + } + } + + button: Widgets.FontIconButton { + hoverEnabled: false + iconName: nodeCard.node.audio.muted ? "volume_off" : "volume_up" + checked: !nodeCard.node.audio.muted + inactiveColor: ShellSettings.colors["surface_container_highest"] + onClicked: { + nodeCard.node.audio.muted = !nodeCard.node.audio.muted; + } + } + + anchors.fill: parent + } + } + } + } + } + } +} diff --git a/bar/volume/DeviceMixer.qml b/bar/volume/DeviceMixer.qml new file mode 100644 index 0000000..ed37a83 --- /dev/null +++ b/bar/volume/DeviceMixer.qml @@ -0,0 +1,64 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import Quickshell.Services.Pipewire +import "../../widgets/" as Widgets +import "../.." + +ColumnLayout { + id: root + + // headphones + // don't load until the node is not null + Loader { + id: sinkLoader + active: sink !== null + Layout.preferredWidth: 350 + Layout.preferredHeight: 45 + + property PwNode sink: Pipewire.defaultAudioSink + + sourceComponent: VolumeCard { + id: sinkCard + node: sinkLoader.sink + button: Widgets.FontIconButton { + hoverEnabled: false + iconName: sinkCard.node.audio.muted ? "volume_off" : "volume_up" + checked: !sinkCard.node.audio.muted + inactiveColor: ShellSettings.colors["surface_container_highest"] + onClicked: { + sinkCard.node.audio.muted = !sinkCard.node.audio.muted; + } + } + + anchors.fill: parent + } + } + + // microphone, same as above + Loader { + id: sourceLoader + active: source !== null + Layout.preferredWidth: 350 + Layout.preferredHeight: 45 + + property PwNode source: Pipewire.defaultAudioSource + + sourceComponent: VolumeCard { + id: sourceCard + node: sourceLoader.source + button: Widgets.FontIconButton { + hoverEnabled: false + iconName: sourceCard.node.audio.muted ? "mic_off" : "mic" + checked: !sourceCard.node.audio.muted + inactiveColor: ShellSettings.colors["surface_container_highest"] + onClicked: { + sourceCard.node.audio.muted = !sourceCard.node.audio.muted; + } + } + + anchors.fill: parent + } + } +} diff --git a/bar/volume/VolumeCard.qml b/bar/volume/VolumeCard.qml new file mode 100644 index 0000000..32ff535 --- /dev/null +++ b/bar/volume/VolumeCard.qml @@ -0,0 +1,50 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell.Widgets +import Quickshell.Services.Pipewire +import "../../widgets/" as Widgets +import "../.." + +WrapperRectangle { + id: root + color: ShellSettings.colors["surface_container"] + radius: width / 2 + margin: 6 + + required property PwNode node + property string text + property Component button + property Component icon + + PwObjectTracker { + id: tracker + objects: [root.node] + } + + RowLayout { + Widgets.MaterialSlider { + value: root.node.audio.volume ?? 0 + text: root.text + icon: root.icon + + onValueChanged: { + // only allow changes when the node is ready other wise you will combust + if (!root.node.ready) + return; + + root.node.audio.volume = value; + } + + Layout.fillWidth: true + Layout.fillHeight: true + } + + Loader { + id: buttonLoader + sourceComponent: root.button + + Layout.preferredWidth: this.height + Layout.fillHeight: true + } + } +} diff --git a/bar/volume/VolumeControl.qml b/bar/volume/VolumeControl.qml new file mode 100644 index 0000000..8df3730 --- /dev/null +++ b/bar/volume/VolumeControl.qml @@ -0,0 +1,76 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import Qt5Compat.GraphicalEffects +import Quickshell.Widgets +import Quickshell.Services.Pipewire +import "../../widgets/" as Widgets +import "../.." + +WrapperItem { + id: root + visible: false + + ColumnLayout { + spacing: 5 + + // Toolbar + Rectangle { + id: toolbar + color: ShellSettings.colors["surface_container_highest"] + radius: 10 + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Rectangle { + width: toolbar.width + height: toolbar.height + radius: toolbar.radius + color: "black" + } + } + + Layout.fillWidth: true + Layout.preferredHeight: 35 + + RowLayout { + spacing: 0 + anchors.fill: parent + + Widgets.FontIconButton { + hoverEnabled: false + iconName: "headphones" + radius: 0 + checked: page.currentIndex === 0 + onClicked: page.currentIndex = 0 + + Layout.fillWidth: true + Layout.fillHeight: true + } + + Widgets.FontIconButton { + hoverEnabled: false + iconName: "tune" + radius: 0 + checked: page.currentIndex === 1 + onClicked: page.currentIndex = 1 + + Layout.fillWidth: true + Layout.fillHeight: true + } + } + } + + StackLayout { + id: page + currentIndex: 0 + Layout.fillWidth: true + Layout.preferredHeight: currentItem ? currentItem.implicitHeight : 0 + + readonly property Item currentItem: children[currentIndex] + + DeviceMixer {} + ApplicationMixer {} + } + } +} diff --git a/bar/volume/VolumeIndicator.qml b/bar/volume/VolumeIndicator.qml new file mode 100644 index 0000000..66f7a6e --- /dev/null +++ b/bar/volume/VolumeIndicator.qml @@ -0,0 +1,27 @@ +import QtQuick +import "../../widgets/" as Widgets + +Item { + id: root + + required property var popup + + Widgets.FontIconButton { + id: button + iconName: "volume_up" + anchors.fill: parent + onClicked: { + if (root.popup.content == volumeMenu) { + root.popup.hide(); + return; + } + + root.popup.set(this, volumeMenu); + root.popup.show(); + } + } + + VolumeControl { + id: volumeMenu + } +} diff --git a/widgets/FontIconButton.qml b/widgets/FontIconButton.qml new file mode 100644 index 0000000..ebe264c --- /dev/null +++ b/widgets/FontIconButton.qml @@ -0,0 +1,48 @@ +import QtQuick +import ".." + +MaterialButton { + id: root + + property real implicitSize + property string iconName: "" + property string activeIconColor: ShellSettings.colors["inverse_primary"] + property string inactiveIconColor: ShellSettings.colors["inverse_surface"] + + implicitWidth: this.implicitSize + implicitHeight: this.implicitSize + + Text { + id: textIcon + text: root.iconName + renderType: Text.NativeRendering + textFormat: Text.PlainText + color: root.containsMouse || root.checked ? root.activeIconColor : root.inactiveIconColor + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + anchors.fill: parent + + font { + family: "Material Symbols Outlined" + pointSize: Math.max(parent.height * 0.60, 11) + + variableAxes: { + "FILL": fill + } + } + + property real fill: !root.containsMouse && !root.checked ? 0 : 1 + + Behavior on fill { + NumberAnimation { + duration: 200 + } + } + + Behavior on color { + ColorAnimation { + duration: 200 + } + } + } +} diff --git a/widgets/MaterialButton.qml b/widgets/MaterialButton.qml index 7ff6793..a9786a9 100644 --- a/widgets/MaterialButton.qml +++ b/widgets/MaterialButton.qml @@ -1,3 +1,24 @@ import QtQuick +import ".." -Rectangle {} +MouseArea { + id: root + hoverEnabled: true + + property real radius: width / 2 + property bool checked: false + property var activeColor: ShellSettings.colors["primary"] + property var inactiveColor: "transparent" + + Rectangle { + color: root.containsMouse || root.checked ? root.activeColor : root.inactiveColor + radius: root.radius + anchors.fill: parent + + Behavior on color { + ColorAnimation { + duration: 200 + } + } + } +} diff --git a/widgets/MaterialSlider.qml b/widgets/MaterialSlider.qml new file mode 100644 index 0000000..4ea95dd --- /dev/null +++ b/widgets/MaterialSlider.qml @@ -0,0 +1,105 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Controls +import Qt5Compat.GraphicalEffects +import ".." + +Slider { + id: root + + value: 0.5 + from: 0.0 + to: 1.0 + + property string text + property Component icon + + background: Rectangle { + id: background + implicitWidth: parent.width + implicitHeight: parent.height + width: root.availableWidth + height: implicitHeight + x: root.leftPadding + y: root.topPadding + root.availableHeight / 2 - height / 2 + z: 0 + color: ShellSettings.colors["surface_container_highest"] + radius: height / 2 + + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Rectangle { + width: background.implicitWidth + height: background.implicitHeight + radius: background.radius + color: "black" + } + } + + Rectangle { + id: visualPos + width: root.visualPosition * (root.availableWidth - root.height) + (root.height / 2) + height: parent.height + color: ShellSettings.colors["primary"] + } + + Text { + id: sliderText + text: root.text + visible: text !== "" + color: ShellSettings.colors["inverse_primary"] + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + + font { + pointSize: Math.max(handle.implicitHeight * 0.35, 11) + } + + anchors { + top: parent.top + bottom: parent.bottom + left: { + let visualWidth = (root.visualPosition * root.availableWidth); + if ((visualWidth / root.availableWidth) < 0.5) + return visualPos.right; + else + return parent.left; + } + right: { + let visualWidth = (root.visualPosition * root.availableWidth); + if ((visualWidth / root.availableWidth) > 0.5) + return visualPos.right; + else + return parent.right; + } + + leftMargin: 20 + rightMargin: 20 + } + } + } + + handle: Rectangle { + id: handle + color: ShellSettings.colors["primary"] + implicitWidth: root.height + implicitHeight: root.height + radius: width / 2 + + x: root.leftPadding + root.visualPosition * (root.availableWidth - width) + y: root.topPadding + root.availableHeight / 2 - height / 2 + // icon maybe + + Loader { + active: root.icon !== undefined + sourceComponent: root.icon + + anchors { + fill: parent + margins: 2 + } + } + } +} diff --git a/widgets/TextButton.qml b/widgets/TextButton.qml deleted file mode 100644 index 25038aa..0000000 --- a/widgets/TextButton.qml +++ /dev/null @@ -1,204 +0,0 @@ -import QtQuick -import Qt5Compat.GraphicalEffects -import Quickshell -import Quickshell.Widgets -import ".." - -Item { - id: root - property string icon - property bool enabled: true - property bool highlight: true - property bool clickable: false - property bool useMask: true - property bool useVariableFill: false - property real size: 18 - property real outerSize: size - property color hoverColor: ShellSettings.colors["inverse_primary"] - property color iconColor: ShellSettings.colors["primary"] - property real buttonRadius: 6 - property real padding: 4 - - property real fillValue: 0.0 - property real fillTarget: 0.0 - property real fillHover: 1.0 - property real fillNormal: 0.0 - property int fillDuration: 300 - - signal clicked(var mouse) - - implicitWidth: outerSize + (padding * 2) - implicitHeight: outerSize + (padding * 2) - - Rectangle { - id: backgroundRect - anchors.fill: parent - radius: root.buttonRadius - color: mouseArea.containsMouse && root.highlight ? root.hoverColor : "transparent" - visible: root.highlight - - Behavior on color { - ColorAnimation { - duration: 200 - easing.type: Easing.InOutCubic - } - } - } - - NumberAnimation { - id: fillAnimation - target: root - property: "fillValue" - duration: root.fillDuration - easing.type: Easing.OutCubic - to: root.fillTarget - } - - Text { - id: variableFillIcon - visible: root.useVariableFill - anchors.centerIn: parent - font.family: "Material Symbols Outlined" - renderType: Text.NativeRendering - textFormat: Text.PlainText - font.pointSize: root.size * 0.8 - text: root.icon - color: root.enabled ?? root.iconColor - opacity: root.enabled ? 1.0 : 0.5 - - font.variableAxes: { - "FILL": root.fillValue.toFixed(1) - } - - Behavior on color { - ColorAnimation { - duration: 200 - easing.type: Easing.InOutCubic - } - } - } - - Item { - id: maskedIcon - visible: root.useMask && !root.useVariableFill - anchors.centerIn: parent - width: root.size - height: root.size - smooth: true - layer.enabled: true - layer.effect: OpacityMask { - source: Rectangle { - width: root.size - height: root.size - color: "white" - } - maskSource: IconImage { - mipmap: true - implicitSize: root.size - source: Quickshell.iconPath(root.icon) - opacity: root.enabled ? 1.0 : 0.5 - smooth: true - } - } - - transform: Translate { - x: mouseArea.containsMouse ? -1 : 0 - y: mouseArea.containsMouse ? -2 : 0 - - Behavior on x { - NumberAnimation { - duration: 450 - easing.type: Easing.OutElastic - easing.amplitude: 1.0 - easing.period: 0.3 - } - } - - Behavior on y { - NumberAnimation { - duration: 450 - easing.type: Easing.OutElastic - easing.amplitude: 1.0 - easing.period: 0.3 - } - } - } - - Rectangle { - anchors.fill: parent - color: root.enabled ?? root.iconColor - - Behavior on color { - ColorAnimation { - duration: 200 - easing.type: Easing.InOutCubic - } - } - } - } - - IconImage { - id: unmaskedIcon - visible: !root.useMask && !root.useVariableFill - anchors.centerIn: parent - source: Quickshell.iconPath(root.icon) - implicitSize: root.size - opacity: root.enabled ? 1.0 : 0.5 - - transform: Translate { - x: mouseArea.containsMouse ? -1 : 0 - y: mouseArea.containsMouse ? -2 : 0 - - Behavior on x { - NumberAnimation { - duration: 350 - easing.type: Easing.OutElastic - easing.amplitude: 1.0 - easing.period: 0.6 - } - } - - Behavior on y { - NumberAnimation { - duration: 350 - easing.type: Easing.OutElastic - easing.amplitude: 1.0 - easing.period: 0.6 - } - } - } - - Behavior on opacity { - NumberAnimation { - duration: 200 - easing.type: Easing.InOutCubic - } - } - } - - MouseArea { - id: mouseArea - anchors.fill: parent - cursorShape: root.clickable ? Qt.PointingHandCursor : "" - enabled: root.clickable && root.enabled - hoverEnabled: true - acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton - - onClicked: function (mouse) { - root.clicked(mouse); - } - - onContainsMouseChanged: { - if (root.useVariableFill) { - root.fillTarget = containsMouse ? root.fillHover : root.fillNormal; - fillAnimation.restart(); - } - } - } - - Component.onCompleted: { - if (useVariableFill) { - fillValue = fillNormal; - } - } -}