From f2a9641a953f6a772281345f92d30cf9c380aacc Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sun, 27 Apr 2025 18:58:06 +0200 Subject: [PATCH] left sidebar foundations --- .config/quickshell/GlobalStates.qml | 1 + .config/quickshell/modules/bar/Bar.qml | 5 + .config/quickshell/modules/bar/Workspaces.qml | 2 +- .../modules/common/widgets/PrimaryTabBar.qml | 86 ++++++++ .../modules/sidebarLeft/SidebarLeft.qml | 196 ++++++++++++++++++ .../sidebarRight/CenterWidgetGroup.qml | 79 +------ .config/quickshell/shell.qml | 2 + 7 files changed, 300 insertions(+), 71 deletions(-) create mode 100644 .config/quickshell/modules/common/widgets/PrimaryTabBar.qml create mode 100644 .config/quickshell/modules/sidebarLeft/SidebarLeft.qml diff --git a/.config/quickshell/GlobalStates.qml b/.config/quickshell/GlobalStates.qml index dffb7f4b..154435e8 100644 --- a/.config/quickshell/GlobalStates.qml +++ b/.config/quickshell/GlobalStates.qml @@ -8,6 +8,7 @@ pragma ComponentBehavior: Bound Singleton { id: root + property int sidebarLeftOpenCount: 0 property int sidebarRightOpenCount: 0 property bool overviewOpen: false property bool workspaceShowNumbers: false diff --git a/.config/quickshell/modules/bar/Bar.qml b/.config/quickshell/modules/bar/Bar.qml index 942e2b0b..d3a06efc 100644 --- a/.config/quickshell/modules/bar/Bar.qml +++ b/.config/quickshell/modules/bar/Bar.qml @@ -199,6 +199,11 @@ Scope { barLeftSideMouseArea.hovered = false barLeftSideMouseArea.trackingScroll = false } + onPressed: (event) => { + if (event.button === Qt.LeftButton) { + openSidebarLeft.running = true + } + } // Scroll to change brightness WheelHandler { onWheel: (event) => { diff --git a/.config/quickshell/modules/bar/Workspaces.qml b/.config/quickshell/modules/bar/Workspaces.qml index ec8d1a64..afb2c1eb 100644 --- a/.config/quickshell/modules/bar/Workspaces.qml +++ b/.config/quickshell/modules/bar/Workspaces.qml @@ -22,7 +22,7 @@ Item { property list workspaceOccupied: [] property int widgetPadding: 4 property int workspaceButtonWidth: 26 - property real workspaceIconSize: workspaceButtonWidth * 0.8 + property real workspaceIconSize: workspaceButtonWidth * 0.75 property real workspaceIconSizeShrinked: workspaceButtonWidth * 0.55 property real workspaceIconOpacityShrinked: 1 property real workspaceIconMarginShrinked: -4 diff --git a/.config/quickshell/modules/common/widgets/PrimaryTabBar.qml b/.config/quickshell/modules/common/widgets/PrimaryTabBar.qml new file mode 100644 index 00000000..97eb3c6f --- /dev/null +++ b/.config/quickshell/modules/common/widgets/PrimaryTabBar.qml @@ -0,0 +1,86 @@ +import "root:/modules/common" +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell + +ColumnLayout { + id: root + spacing: 0 + required property var tabButtonList // Something like [{"icon": "notifications", "name": qsTr("Notifications")}, {"icon": "volume_up", "name": qsTr("Volume mixer")}] + required property var externalTrackedTab + property bool enableIndicatorAnimation: false + signal currentIndexChanged(int index) + + TabBar { + id: tabBar + Layout.fillWidth: true + currentIndex: root.externalTrackedTab + onCurrentIndexChanged: root.onCurrentIndexChanged(currentIndex) + + background: Item { + WheelHandler { + onWheel: (event) => { + if (event.angleDelta.y < 0) + tabBar.currentIndex = Math.min(tabBar.currentIndex + 1, root.tabButtonList.length - 1) + else if (event.angleDelta.y > 0) + tabBar.currentIndex = Math.max(tabBar.currentIndex - 1, 0) + } + acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad + } + } + + Repeater { + model: root.tabButtonList + delegate: PrimaryTabButton { + selected: (index == root.externalTrackedTab) + buttonText: modelData.name + buttonIcon: modelData.icon + } + } + } + + Item { // Tab indicator + id: tabIndicator + Layout.fillWidth: true + height: 3 + Connections { + target: root + function onExternalTrackedTabChanged() { + root.enableIndicatorAnimation = true + } + } + Rectangle { + color: Appearance.m3colors.m3primary + radius: Appearance.rounding.full + z: 2 + + anchors.fill: parent + anchors.leftMargin: { + const tabCount = root.tabButtonList.length + const targetWidth = tabBar.contentItem.children[0].children[tabBar.currentIndex].tabContentWidth + const fullTabSize = root.width / tabCount; + return fullTabSize * root.externalTrackedTab + (fullTabSize - targetWidth) / 2; + } + anchors.rightMargin: { + const tabCount = root.tabButtonList.length + const targetWidth = tabBar.contentItem.children[0].children[tabBar.currentIndex].tabContentWidth + const fullTabSize = root.width / tabCount; + return fullTabSize * (tabCount - root.externalTrackedTab - 1) + (fullTabSize - targetWidth) / 2; + } + Behavior on anchors.leftMargin { + enabled: root.enableIndicatorAnimation + SmoothedAnimation { + velocity: Appearance.animation.positionShift.velocity + } + } + Behavior on anchors.rightMargin { + enabled: root.enableIndicatorAnimation + SmoothedAnimation { + velocity: Appearance.animation.positionShift.velocity + } + } + + } + } +} diff --git a/.config/quickshell/modules/sidebarLeft/SidebarLeft.qml b/.config/quickshell/modules/sidebarLeft/SidebarLeft.qml new file mode 100644 index 00000000..0a2e1f8e --- /dev/null +++ b/.config/quickshell/modules/sidebarLeft/SidebarLeft.qml @@ -0,0 +1,196 @@ +import "root:/" +import "root:/services" +import "root:/modules/common" +import "root:/modules/common/widgets" +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell.Io +import Quickshell +import Quickshell.Widgets +import Quickshell.Wayland +import Quickshell.Hyprland +import Qt5Compat.GraphicalEffects + +Scope { // Scope + id: root + property int sidebarWidth: Appearance.sizes.sidebarWidth + property int sidebarPadding: 15 + property var tabButtonList: [{"icon": "neurology", "name": qsTr("LLMs")}, {"icon": "flare", "name": qsTr("Waifus")}] + + Variants { // Window repeater + id: sidebarVariants + model: Quickshell.screens + + PanelWindow { // Window + id: sidebarRoot + visible: false + focusable: true + property int currentTab: 0 + + onVisibleChanged: { + GlobalStates.sidebarLeftOpenCount += visible ? 1 : -1 + } + + property var modelData + + screen: modelData + exclusiveZone: 0 + width: sidebarWidth + WlrLayershell.namespace: "quickshell:sidebarLeft" + WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive + color: "transparent" + + anchors { + top: true + left: true + bottom: true + } + + HyprlandFocusGrab { // Click outside to close + id: grab + windows: [ sidebarRoot ] + active: false + onCleared: () => { + if (!active) sidebarRoot.visible = false + } + } + + Connections { + target: sidebarRoot + function onVisibleChanged() { + delayedGrabTimer.start() + } + } + + Timer { + id: delayedGrabTimer + interval: ConfigOptions.hacks.arbitraryRaceConditionDelay + repeat: false + onTriggered: { + grab.active = sidebarRoot.visible + } + } + + // Background + Rectangle { + id: sidebarLeftBackground + + anchors.centerIn: parent + width: parent.width - Appearance.sizes.hyprlandGapsOut * 2 + height: parent.height - Appearance.sizes.hyprlandGapsOut * 2 + color: Appearance.colors.colLayer0 + radius: Appearance.rounding.screenRounding - Appearance.sizes.elevationMargin + 1 + + Keys.onPressed: (event) => { + if (event.key === Qt.Key_Escape) { + sidebarRoot.visible = false; + } + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: sidebarPadding + + spacing: sidebarPadding + + PrimaryTabBar { // Tab strip + id: tabBar + tabButtonList: root.tabButtonList + externalTrackedTab: sidebarRoot.currentTab + function onCurrentIndexChanged(currentIndex) { + sidebarRoot.currentTab = currentIndex + } + } + + SwipeView { // Content pages + id: swipeView + Layout.topMargin: 5 + Layout.fillWidth: true + Layout.fillHeight: true + currentIndex: currentTab + onCurrentIndexChanged: { + tabBar.enableIndicatorAnimation = true + sidebarRoot.currentTab = currentIndex + } + + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Rectangle { + width: swipeView.width + height: swipeView.height + radius: Appearance.rounding.small + } + } + + Item {} + Item {} + } + + } + } + + // Shadow + DropShadow { + anchors.fill: sidebarLeftBackground + horizontalOffset: 0 + verticalOffset: 2 + radius: Appearance.sizes.elevationMargin + samples: Appearance.sizes.elevationMargin * 2 + 1 // Ideally should be 2 * radius + 1, see qt docs + color: Appearance.colors.colShadow + source: sidebarLeftBackground + } + + } + + } + + IpcHandler { + target: "sidebarLeft" + + function toggle(): void { + for (let i = 0; i < sidebarVariants.instances.length; i++) { + let panelWindow = sidebarVariants.instances[i]; + if (panelWindow.modelData.name == Hyprland.focusedMonitor.name) { + panelWindow.visible = !panelWindow.visible; + if(panelWindow.visible) Notifications.timeoutAll(); + } + } + } + + function close(): void { + for (let i = 0; i < sidebarVariants.instances.length; i++) { + let panelWindow = sidebarVariants.instances[i]; + if (panelWindow.modelData.name == Hyprland.focusedMonitor.name) { + panelWindow.visible = false; + } + } + } + + function open(): void { + for (let i = 0; i < sidebarVariants.instances.length; i++) { + let panelWindow = sidebarVariants.instances[i]; + if (panelWindow.modelData.name == Hyprland.focusedMonitor.name) { + panelWindow.visible = true; + if(panelWindow.visible) Notifications.timeoutAll(); + } + } + } + } + + GlobalShortcut { + name: "sidebarLeftToggle" + description: "Toggles left sidebar on press" + + onPressed: { + for (let i = 0; i < sidebarVariants.instances.length; i++) { + let panelWindow = sidebarVariants.instances[i]; + if (panelWindow.modelData.name == Hyprland.focusedMonitor.name) { + panelWindow.visible = !panelWindow.visible; + if(panelWindow.visible) Notifications.timeoutAll(); + } + } + } + } + +} diff --git a/.config/quickshell/modules/sidebarRight/CenterWidgetGroup.qml b/.config/quickshell/modules/sidebarRight/CenterWidgetGroup.qml index be69cd5b..1503ef10 100644 --- a/.config/quickshell/modules/sidebarRight/CenterWidgetGroup.qml +++ b/.config/quickshell/modules/sidebarRight/CenterWidgetGroup.qml @@ -35,76 +35,12 @@ Rectangle { anchors.fill: parent spacing: 0 - TabBar { + PrimaryTabBar { id: tabBar - Layout.fillWidth: true - currentIndex: currentTab - onCurrentIndexChanged: currentTab = currentIndex - - background: Item { - WheelHandler { - onWheel: (event) => { - if (event.angleDelta.y < 0) - tabBar.currentIndex = Math.min(tabBar.currentIndex + 1, root.tabButtonList.length - 1) - else if (event.angleDelta.y > 0) - tabBar.currentIndex = Math.max(tabBar.currentIndex - 1, 0) - } - acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad - } - } - - Repeater { - model: root.tabButtonList - delegate: PrimaryTabButton { - selected: (index == currentTab) - buttonText: modelData.name - buttonIcon: modelData.icon - } - } - } - - Item { // Tab indicator - id: tabIndicator - Layout.fillWidth: true - height: 3 - property bool enableIndicatorAnimation: false - Connections { - target: root - function onCurrentTabChanged() { - tabIndicator.enableIndicatorAnimation = true - } - } - Rectangle { - color: Appearance.m3colors.m3primary - radius: Appearance.rounding.full - z: 2 - - anchors.fill: parent - anchors.leftMargin: { - const tabCount = root.tabButtonList.length - const targetWidth = tabBar.contentItem.children[0].children[tabBar.currentIndex].tabContentWidth - const fullTabSize = tabBar.width / tabCount; - return fullTabSize * currentTab + (fullTabSize - targetWidth) / 2; - } - anchors.rightMargin: { - const tabCount = root.tabButtonList.length - const targetWidth = tabBar.contentItem.children[0].children[tabBar.currentIndex].tabContentWidth - const fullTabSize = tabBar.width / tabCount; - return fullTabSize * (tabCount - currentTab - 1) + (fullTabSize - targetWidth) / 2; - } - Behavior on anchors.leftMargin { - enabled: tabIndicator.enableIndicatorAnimation - SmoothedAnimation { - velocity: Appearance.animation.positionShift.velocity - } - } - Behavior on anchors.rightMargin { - enabled: tabIndicator.enableIndicatorAnimation - SmoothedAnimation { - velocity: Appearance.animation.positionShift.velocity - } - } - + tabButtonList: root.tabButtonList + externalTrackedTab: root.currentTab + function onCurrentIndexChanged(currentIndex) { + root.currentTab = currentIndex } } @@ -114,7 +50,10 @@ Rectangle { Layout.fillWidth: true Layout.fillHeight: true currentIndex: currentTab - onCurrentIndexChanged: currentTab = currentIndex + onCurrentIndexChanged: { + tabBar.enableIndicatorAnimation = true + root.currentTab = currentIndex + } layer.enabled: true layer.effect: OpacityMask { diff --git a/.config/quickshell/shell.qml b/.config/quickshell/shell.qml index 72cf366a..8de63090 100644 --- a/.config/quickshell/shell.qml +++ b/.config/quickshell/shell.qml @@ -6,6 +6,7 @@ import "./modules/onScreenDisplay/" import "./modules/overview/" import "./modules/screenCorners/" import "./modules/session/" +import "./modules/sidebarLeft/" import "./modules/sidebarRight/" import QtQuick import QtQuick.Controls @@ -27,6 +28,7 @@ ShellRoot { ReloadPopup {} ScreenCorners {} Session {} + SidebarLeft {} SidebarRight {} }