progress maybe, maybe not

This commit is contained in:
kossLAN 2025-07-04 02:16:35 -04:00
parent ce6c1f410c
commit 83a0ac8899
Signed by: kossLAN
SSH key fingerprint: SHA256:bdV0x+wdQHGJ6LgmstH3KV8OpWY+OOFmJcPcB0wQPV8
21 changed files with 1412 additions and 49 deletions

View file

@ -6,12 +6,13 @@ import Quickshell.Io
Singleton { Singleton {
property alias settings: jsonAdapter.settings property alias settings: jsonAdapter.settings
property alias sizing: jsonAdapter.sizing
property alias colors: jsonAdapter.colors property alias colors: jsonAdapter.colors
FileView { FileView {
path: `${Quickshell.env("XDG_DATA_HOME")}/quickshell/settings.json` path: `${Quickshell.env("XDG_DATA_HOME")}/quickshell/settings.json`
watchChanges: true watchChanges: true
// onFileChanged: reload() onFileChanged: reload()
onAdapterUpdated: writeAdapter() onAdapterUpdated: writeAdapter()
blockLoading: true blockLoading: true
@ -19,12 +20,18 @@ Singleton {
id: jsonAdapter id: jsonAdapter
property JsonObject settings: JsonObject { property JsonObject settings: JsonObject {
property int barHeight: 25
property string wallpaperUrl: Qt.resolvedUrl("root:resources/wallpapers/pixelart0.jpg") property string wallpaperUrl: Qt.resolvedUrl("root:resources/wallpapers/pixelart0.jpg")
property string colorScheme: "scheme-fruit-salad" property string colorScheme: "scheme-fruit-salad"
property string screenshotPath: "/home/koss/Pictures" property string screenshotPath: "/home/koss/Pictures"
} }
property JsonObject sizing: JsonObject {
property int borderWidth: 5
property int topBorderWidth: 20
property int gaps: 5
}
property var colors: { property var colors: {
"background": "#131313", "background": "#131313",
"error": "#ffb4ab", "error": "#ffb4ab",

View file

@ -2,10 +2,8 @@ pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Quickshell.Widgets import Quickshell.Widgets
import "../../widgets/" as Widgets import "../../widgets/" as Widgets
import "../.."
WrapperItem { WrapperItem {
id: root id: root
@ -14,55 +12,16 @@ WrapperItem {
ColumnLayout { ColumnLayout {
spacing: 10 spacing: 10
// Toolbar Widgets.TabBar {
Rectangle { id: tabBar
id: toolbar model: ["headphones", "tune"]
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.fillWidth: true
Layout.preferredHeight: 30 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 { StackLayout {
id: page id: page
currentIndex: 0 currentIndex: tabBar.currentIndex
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: currentItem ? currentItem.implicitHeight : 0 Layout.preferredHeight: currentItem ? currentItem.implicitHeight : 0

View file

@ -0,0 +1,12 @@
import QtQuick
import Quickshell.Wayland
import ".."
Text {
id: windowText
text: ToplevelManager.activeToplevel?.title ?? ""
color: ShellSettings.colors["inverse_surface"]
font.pointSize: 11
visible: text !== ""
elide: Text.ElideRight
}

View file

@ -0,0 +1,117 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Effects
import Quickshell
import ".."
Scope {
id: root
required property var screen
property alias topWindow: topPanel
property alias top: topPanel.data
PanelWindow {
id: overlay
color: "transparent"
screen: root.modelData
mask: Region {}
anchors {
left: true
right: true
top: true
bottom: true
}
Item {
anchors.fill: parent
Rectangle {
anchors.fill: parent
color: ShellSettings.colors["surface"]
// visible: false
layer.enabled: true
layer.effect: MultiEffect {
maskEnabled: true
maskSource: mask
maskInverted: true // Changed from true to false
maskThresholdMin: 0.5
maskSpreadAtMin: 1
}
}
Item {
id: mask
anchors.fill: parent
layer.enabled: true
visible: false
Rectangle {
color: "white"
radius: 15
anchors {
fill: parent
margins: ShellSettings.sizing.borderWidth
topMargin: ShellSettings.sizing.topBorderWidth
}
}
}
}
}
PanelWindow {
id: topPanel
screen: root.modelData
color: "transparent"
implicitHeight: ShellSettings.sizing.topBorderWidth
anchors {
top: true
left: true
right: true
}
}
PanelWindow {
id: bottomPanel
screen: root.modelData
color: "transparent"
implicitHeight: ShellSettings.sizing.borderWidth
anchors {
bottom: true
left: true
right: true
}
}
PanelWindow {
id: leftPanel
screen: root.modelData
color: "transparent"
implicitWidth: ShellSettings.sizing.borderWidth
anchors {
top: true
bottom: true
left: true
}
}
PanelWindow {
id: rightPanel
screen: root.modelData
color: "transparent"
implicitWidth: ShellSettings.sizing.borderWidth
anchors {
top: true
bottom: true
right: true
}
}
}

View file

@ -0,0 +1,21 @@
import QtQuick
import Quickshell
Text {
property string ap: sysClock.hours >= 12 ? "PM" : "AM"
property string minutes: sysClock.minutes.toString().padStart(2, '0')
property string hours: {
var value = sysClock.hours % 12;
if (value === 0)
return 12;
return value;
}
SystemClock {
id: sysClock
enabled: true
}
text: `${hours}:${minutes} ${ap}`
font.pointSize: 11
}

View file

@ -0,0 +1,102 @@
pragma ComponentBehavior: Bound
import Quickshell
import QtQuick
import QtQuick.Layouts
import "power"
import "volume"
import "systray" as SysTray
import "popups" as Popup
import "../widgets"
import ".."
Scope {
id: root
Variants {
model: Quickshell.screens
Border {
id: border
screen: modelData
required property var modelData
top: RowLayout {
id: top
spacing: 0
anchors {
fill: parent
leftMargin: 8
rightMargin: 8
}
Popup.MenuWindow {
id: popupWindow
bar: border.topWindow
}
RowLayout {
spacing: 5
Layout.fillWidth: true
Layout.fillHeight: true
Workspaces {
screen: border.screen
Layout.fillHeight: true
}
Separator {
visible: activeWindow.visible
Layout.leftMargin: 5
Layout.rightMargin: 5
}
ActiveWindow {
id: activeWindow
Layout.preferredWidth: 400
}
}
RowLayout {
spacing: 5
Layout.fillWidth: true
Layout.fillHeight: true
Layout.alignment: Qt.AlignRight
SysTray.SysTray {
id: sysTray
popup: popupWindow
Layout.fillHeight: true
}
VolumeIndicator {
id: volumeIndicator
popup: popupWindow
Layout.preferredWidth: this.height
Layout.fillHeight: true
Layout.topMargin: 2
Layout.bottomMargin: 2
}
BatteryIndicator {
id: batteryIndicator
popup: popupWindow
Layout.fillHeight: true
}
Separator {
// Layout.leftMargin: 5
Layout.rightMargin: 5
}
Clock {
id: clock
color: ShellSettings.colors["inverse_surface"]
}
}
}
}
}
}

View file

@ -0,0 +1,73 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Hyprland
import ".."
RowLayout {
spacing: 6
visible: Hyprland.monitors.values.length != 0
required property var screen
Repeater {
id: workspaceButtons
model: ScriptModel {
values: Hyprland.workspaces.values.slice().filter(
workspace => workspace.monitor === Hyprland.monitorFor(screen)
)
}
Rectangle {
required property var modelData
radius: height / 2
Layout.alignment: Qt.AlignVCenter
Layout.preferredHeight: 12
Layout.preferredWidth: {
if (Hyprland.focusedMonitor?.activeWorkspace?.id === modelData?.id)
return 25;
return 12;
}
color: {
let value = Qt.color(ShellSettings.colors["secondary"]).darker(2);
if (!modelData?.id || !Hyprland.focusedMonitor?.activeWorkspace?.id)
return value;
if (workspaceButton.containsMouse) {
value = ShellSettings.colors["on_primary"];
} else if (Hyprland.focusedMonitor.activeWorkspace.id === modelData.id) {
value = ShellSettings.colors["primary"];
}
return value;
}
Behavior on Layout.preferredWidth {
SmoothedAnimation {
duration: 150
velocity: 200
easing.type: Easing.OutCubic
}
}
Behavior on color {
ColorAnimation {
duration: 100
easing.type: Easing.OutQuad
}
}
MouseArea {
id: workspaceButton
anchors.fill: parent
hoverEnabled: true
onPressed: Hyprland.dispatch(`workspace ${parent.modelData.id}`)
}
}
}
}

View file

@ -0,0 +1,70 @@
pragma ComponentBehavior: Bound
import QtQuick
import Qt5Compat.GraphicalEffects
import Quickshell.Widgets
import "../.."
Item {
id: root
required property var bar
property var implicitSize: 0
readonly property real actualSize: Math.min(root.width, root.height)
implicitWidth: parent.height
implicitHeight: parent.height
NotificationCenter {
id: notificationCenter
}
Rectangle {
color: mouseArea.containsMouse ? ShellSettings.colors["primary"] : "transparent"
radius: 5
anchors {
fill: parent
margins: 1
}
}
MouseArea {
id: mouseArea
hoverEnabled: true
anchors.fill: parent
onPressed: {
if (notificationCenter.visible) {
notificationCenter.hide();
} else {
notificationCenter.show();
}
}
}
Item {
implicitWidth: root.implicitSize
implicitHeight: root.implicitSize
anchors.centerIn: parent
layer.enabled: true
layer.effect: OpacityMask {
source: Rectangle {
width: root.actualSize
height: root.actualSize
color: "white"
}
maskSource: IconImage {
implicitSize: root.actualSize
source: "root:resources/general/notification.svg"
}
}
Rectangle {
color: mouseArea.containsMouse ? ShellSettings.colors["inverse_primary"] : ShellSettings.colors["inverse_surface"]
anchors.fill: parent
}
}
// TODO: notification number overlay
}

View file

@ -0,0 +1,233 @@
import Quickshell
import Quickshell.Hyprland
import Quickshell.Widgets
import QtQuick
import QtQuick.Shapes
// import QtQuick.Effects
import "../.."
// In need of heavy refactor
PopupWindow {
id: root
color: "transparent"
implicitWidth: bar.width
implicitHeight: Math.max(popupContainer.height, 800) + 20
mask: Region {
item: popupContainer
}
anchor {
window: bar
rect: Qt.rect(0, 0, bar.width, bar.height)
edges: Edges.Bottom | Edges.Left
gravity: Edges.Bottom | Edges.Right
adjustment: PopupAdjustment.None
}
required property var bar
property var isOpen: false
property var padding: ShellSettings.sizing.borderWidth
property var radius: 12
property var item
property var content
function set(item, content) {
root.item = item;
root.content = content;
popupContent.data = content;
let itemPos = item.mapToItem(root.bar.contentItem, 0, root.bar.height, item.width, 0).x;
position(itemPos);
popupContainer.opacity = 0;
popupContent.opacity = 0;
}
function position(itemPos) {
if (itemPos === undefined)
return;
let rightEdge = itemPos + popupContainer.implicitWidth;
let maxRightEdge = root.width - padding;
let isTouchingRightEdge = rightEdge > maxRightEdge;
if (isTouchingRightEdge) {
// touching right edge, reposition
// console.log("touching right edge");
popupContainer.x = maxRightEdge - popupContainer.implicitWidth;
popupContainer.y = 0;
popupContainer.bottomLeftRadius = radius;
popupContainer.bottomRightRadius = 0;
} else {
// not touching right edge
popupContainer.x = itemPos;
popupContainer.y = 0;
popupContainer.bottomLeftRadius = radius;
popupContainer.bottomRightRadius = radius;
}
}
function show() {
grab.active = true;
isOpen = true;
root.visible = true; // set and leave open
root.content.visible = true;
popupContainer.opacity = 1;
popupContent.opacity = 1;
}
function hide() {
grab.active = false;
isOpen = false;
popupContainer.opacity = 0;
popupContent.opacity = 0;
root.item = undefined;
root.content = undefined;
popupContent.data = [];
}
function toggle() {
if (isOpen) {
hide();
} else {
show();
}
}
// RectangularShadow {
// radius: popupContainer.radius
// anchors.fill: popupContainer
// opacity: popupContainer.opacity
// visible: popupContainer.visible
// blur: 10
// spread: 2
// }
Shape {
id: shapeContainer
// anchors.fill: popupContainer
width: implicitWidth
height: implicitHeight
opacity: popupContainer.opacity
WrapperRectangle {
id: popupContainer
color: ShellSettings.colors["surface"]
margin: 8
clip: true
opacity: 0
// visible: opacity > 0
// x: root.bar.width
// spooky, likely to cause problems lol
width: implicitWidth
height: implicitHeight
onVisibleChanged: root.visible = visible
// needed to handle occurrences where items are resized while open
onImplicitWidthChanged: {
if (root.isOpen && popupContent.data !== []) {
// console.log("repositioning popup");
let itemPos = root.item.mapToItem(root.bar.contentItem, 0, root.bar.height, root.item.width, 0).x;
root.position(itemPos);
}
}
Item {
id: popupContent
implicitWidth: Math.max(root.content?.width, 60)
implicitHeight: Math.max(childrenRect.height, 60)
Behavior on opacity {
NumberAnimation {
duration: 200
easing.type: Easing.Linear
from: 0
to: 1
}
}
}
HyprlandFocusGrab {
id: grab
windows: [root, root.bar]
onCleared: {
root.hide();
}
}
Behavior on width {
enabled: root.isOpen
SmoothedAnimation {
duration: 200
easing.type: Easing.Linear
}
}
Behavior on height {
SmoothedAnimation {
duration: 200
easing.type: Easing.Linear
}
}
Behavior on x {
enabled: root.isOpen
SmoothedAnimation {
duration: 200
easing.type: Easing.OutQuad
}
}
}
ShapePath {
strokeWidth: -1
fillColor: ShellSettings.colors["surface"]
startX: popupContainer.x - 25
startY: popupContainer.y
PathLine {
relativeX: 25
relativeY: 0
}
PathLine {
relativeX: 0
relativeY: 25
}
PathArc {
direction: PathArc.Counterclockwise
relativeX: -25
relativeY: -25
radiusX: 25
radiusY: 25
useLargeArc: false
}
// PathLine {
// x: 0
// y: 12
// } // Vertical line down
// PathLine {
// x: 12
// y: 12
// } // Horizontal line to the right
// PathLine {
// x: 12
// y: 0
// } // Horizontal line back to the top
}
Behavior on opacity {
NumberAnimation {
duration: 200
easing.type: Easing.Linear
}
}
}
}

View file

@ -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));
// }
// }
}
}
}

View file

@ -0,0 +1,96 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
import Quickshell.Services.SystemTray
import "../../widgets" as Widgets
RowLayout {
id: root
spacing: 5
visible: SystemTray.items.values.length > 0
required property var popup
Repeater {
model: SystemTray.items
delegate: Item {
id: trayField
Layout.preferredWidth: parent.height
Layout.fillHeight: true
required property SystemTrayItem modelData
Widgets.MaterialButton {
id: trayButton
hoverEnabled: true
onClicked: {
menuOpener.menu = trayField.modelData.menu;
if (root.popup.content == trayMenu) {
root.popup.hide();
return;
}
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 {
id: menuOpener
}
WrapperItem {
id: trayMenu
visible: false
property var leftItem: false
property var rightItem: false
ColumnLayout {
id: menuContainer
spacing: 2
Repeater {
model: menuOpener.children
delegate: TrayMenuItem {
id: sysTrayContent
Layout.fillWidth: true
Layout.fillHeight: true
rootMenu: trayMenu
onInteracted: {
root.popup.hide();
menuOpener.menu = null;
}
}
}
}
}
}
}
}

View file

@ -0,0 +1,170 @@
import Quickshell
import Quickshell.Widgets
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import "../../widgets" as Widgets
import "../.."
ColumnLayout {
id: root
required property QsMenuEntry menuData
required property var rootMenu
signal interacted
Component.onCompleted: {
if (menuData?.buttonType !== QsMenuButtonType.None || menuData?.icon != "") {
rootMenu.leftItem = true;
}
if (menuData?.hasChildren) {
rootMenu.rightItem = true;
}
}
WrapperRectangle {
Layout.fillWidth: true
Layout.preferredHeight: 25
radius: 6
color: {
if (!root.menuData?.enabled)
return "transparent";
if (entryArea.containsMouse)
return ShellSettings.colors["primary"];
return "transparent";
}
WrapperMouseArea {
id: entryArea
hoverEnabled: true
anchors.fill: parent
onClicked: {
if (!root.menuData?.enabled)
return;
if (root.menuData?.hasChildren) {
subTrayMenu.visible = !subTrayMenu.visible;
return;
}
root.menuData?.triggered();
root.interacted();
}
RowLayout {
id: menuEntry
spacing: 5
Layout.fillWidth: true
Item {
visible: root.rootMenu.leftItem
Layout.preferredWidth: 20
Layout.fillHeight: true
Layout.alignment: Qt.AlignVCenter
Layout.leftMargin: 5
RadioButton {
id: radioButton
visible: (root.menuData?.buttonType === QsMenuButtonType.RadioButton) ?? false
checked: (root.menuData?.checkState) ?? false
anchors.centerIn: parent
}
CheckBox {
id: checkBox
visible: (root.menuData?.buttonType === QsMenuButtonType.CheckBox) ?? false
checked: (root.menuData?.checkState) ?? false
anchors.centerIn: parent
}
IconImage {
id: entryImage
visible: (root.menuData?.buttonType === QsMenuButtonType.None && root.menuData?.icon !== "") ?? false
source: (root.menuData?.icon) ?? ""
anchors.fill: parent
}
}
Text {
id: text
text: root.menuData?.text ?? ""
verticalAlignment: Text.AlignVCenter
color: {
let color = Qt.color(ShellSettings.colors["inverse_surface"]);
if (!root.menuData?.enabled)
return color.darker(2);
if (entryArea.containsMouse)
return Qt.color(ShellSettings.colors["inverse_primary"]);
return color;
}
Layout.fillWidth: true
Layout.fillHeight: true
}
Item {
visible: root.rootMenu.rightItem
Layout.preferredHeight: 20
Layout.preferredWidth: 20
Layout.rightMargin: 5
Widgets.IconButton {
id: arrowButton
visible: root.menuData?.hasChildren ?? false
activeRectangle: false
source: "root:resources/general/right-arrow.svg"
rotation: subTrayMenu.visible ? 90 : 0
anchors.fill: parent
Behavior on rotation {
NumberAnimation {
duration: 150
easing.type: Easing.OutCubic
}
}
onClicked: {
root.expanded = !root.expanded;
}
}
}
}
}
}
WrapperRectangle {
id: subTrayMenu
color: ShellSettings.colors["surface_container"]
radius: 8
visible: false
Layout.fillWidth: true
QsMenuOpener {
id: menuOpener
menu: root.menuData
}
ColumnLayout {
id: subTrayContainer
spacing: 2
Layout.fillWidth: true
Repeater {
model: menuOpener.children
delegate: BoundComponent {
id: subMenuEntry
source: "TrayMenuItem.qml"
Layout.fillWidth: true
required property var modelData
property var rootMenu: root.rootMenu
}
}
}
}
}

View file

@ -0,0 +1,29 @@
import Quickshell
import QtQuick
import QtQuick.Layouts
import "../.."
ColumnLayout {
id: root
required property QsMenuEntry modelData
required property var rootMenu
property var leftItem
signal interacted
Rectangle {
visible: (root.modelData?.isSeparator ?? false)
color: ShellSettings.colors["surface_container_high"]
Layout.fillWidth: true
Layout.preferredHeight: 2
Layout.leftMargin: 8
Layout.rightMargin: 8
}
TrayMenuEntry {
visible: !root.modelData?.isSeparator
rootMenu: root.rootMenu
menuData: root.modelData
Layout.fillWidth: true
onInteracted: root.interacted()
}
}

View file

@ -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
}
}
}
}
}
}
}

View file

@ -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
}
}
}

View file

@ -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
}
}
}

View file

@ -0,0 +1,34 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell.Widgets
import "../../widgets/" as Widgets
WrapperItem {
id: root
visible: false
ColumnLayout {
spacing: 10
Widgets.TabBar {
id: tabBar
model: ["headphones", "tune"]
Layout.fillWidth: true
Layout.preferredHeight: 35
}
StackLayout {
id: page
currentIndex: tabBar.currentIndex
Layout.fillWidth: true
Layout.preferredHeight: currentItem ? currentItem.implicitHeight : 0
readonly property Item currentItem: children[currentIndex]
DeviceMixer {}
ApplicationMixer {}
}
}
}

View file

@ -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
}
}

View file

@ -3,7 +3,8 @@
import Quickshell import Quickshell
import QtQuick import QtQuick
import "bar" as Bar // import "bar" as Bar
import "experimental-bar" as Bar
import "notifications" as Notifications import "notifications" as Notifications
import "mpris" as Mpris import "mpris" as Mpris
import "volume-osd" as VolumeOSD import "volume-osd" as VolumeOSD

View file

@ -0,0 +1,34 @@
import QtQuick
import ".."
Text {
id: textIcon
property real fill: 0
renderType: Text.NativeRendering
textFormat: Text.PlainText
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
font {
family: "Material Symbols Outlined"
pointSize: Math.max(parent.height * 0.50, 11)
variableAxes: {
"FILL": fill
}
}
Behavior on fill {
NumberAnimation {
duration: 200
}
}
Behavior on color {
ColorAnimation {
duration: 200
}
}
}

92
shell/widgets/TabBar.qml Normal file
View file

@ -0,0 +1,92 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import ".."
Item {
id: root
property alias model: buttonRepeater.model
property int currentIndex: 0
RowLayout {
id: buttonGroup
spacing: 0
anchors.fill: parent
Repeater {
id: buttonRepeater
delegate: MouseArea {
id: button
hoverEnabled: true
Layout.fillWidth: true
Layout.fillHeight: true
Layout.alignment: Qt.AlignHCenter | Qt.AlignTop
required property var modelData
required property int index
property bool checked: index === root.currentIndex
onClicked: {
currentIndex = index;
root.updateSelectionBarPosition();
}
FontIcon {
text: button.modelData
fill: {
if (button.checked)
return 1;
return button.containsMouse ? 1 : 0;
}
color: button.checked ? ShellSettings.colors["primary"] : ShellSettings.colors["inverse_surface"]
anchors.fill: parent
anchors.bottomMargin: 5
}
}
}
}
Rectangle {
id: selectionBar
implicitWidth: 100
implicitHeight: 3
topLeftRadius: width / 2
topRightRadius: width / 2
color: ShellSettings.colors["primary"]
anchors.bottom: tabBar.top
Behavior on x {
NumberAnimation {
duration: 250
easing.type: Easing.OutCubic
}
}
}
Rectangle {
id: tabBar
implicitHeight: 1.5
radius: width / 2
color: ShellSettings.colors["surface_container"]
anchors {
top: buttonGroup.bottom
left: parent.left
right: parent.right
}
}
function updateSelectionBarPosition() {
if (buttonRepeater.count > 0) {
var buttonWidth = buttonGroup.width / buttonRepeater.count;
var targetX = currentIndex * buttonWidth + (buttonWidth - selectionBar.width) / 2;
selectionBar.x = targetX;
}
}
Component.onCompleted: updateSelectionBarPosition()
onWidthChanged: updateSelectionBarPosition()
}