From 85d575f6c18017693cb2f3ab07314a0cf43adbe6 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 18 Sep 2025 15:19:15 +1000 Subject: feat: add sidebar WIP --- README.md | 5 +++ config/Config.qml | 2 + config/SidebarConfig.qml | 11 ++++++ modules/drawers/Backgrounds.qml | 17 +++++++- modules/drawers/Drawers.qml | 4 +- modules/drawers/Interactions.qml | 77 ++++++++++++++++++++++++++---------- modules/drawers/Panels.qml | 23 +++++++++-- modules/notifications/Background.qml | 15 +++---- modules/notifications/Content.qml | 8 ++-- modules/notifications/Wrapper.qml | 28 ++++++++++--- modules/session/Wrapper.qml | 6 ++- modules/sidebar/Background.qml | 52 ++++++++++++++++++++++++ modules/sidebar/Content.qml | 19 +++++++++ modules/sidebar/Wrapper.qml | 66 +++++++++++++++++++++++++++++++ modules/utilities/Background.qml | 7 ++-- modules/utilities/Wrapper.qml | 11 ++++-- 16 files changed, 296 insertions(+), 55 deletions(-) create mode 100644 config/SidebarConfig.qml create mode 100644 modules/sidebar/Background.qml create mode 100644 modules/sidebar/Content.qml create mode 100644 modules/sidebar/Wrapper.qml diff --git a/README.md b/README.md index 89281d2..1c952f7 100644 --- a/README.md +++ b/README.md @@ -516,6 +516,7 @@ default, you must create it manually. }, "session": { "dragThreshold": 30, + "enabled": true, "vimKeybinds": false, "commands": { "logout": ["loginctl", "terminate-user", ""], @@ -524,6 +525,10 @@ default, you must create it manually. "reboot": ["systemctl", "reboot"] } }, + "sidebar": { + "dragThreshold": 80, + "enabled": true + }. "utilities": { "enabled": true } diff --git a/config/Config.qml b/config/Config.qml index b23a4e2..f33cbba 100644 --- a/config/Config.qml +++ b/config/Config.qml @@ -21,6 +21,7 @@ Singleton { property alias winfo: adapter.winfo property alias lock: adapter.lock property alias utilities: adapter.utilities + property alias sidebar: adapter.sidebar property alias services: adapter.services property alias paths: adapter.paths @@ -46,6 +47,7 @@ Singleton { property WInfoConfig winfo: WInfoConfig {} property LockConfig lock: LockConfig {} property UtilitiesConfig utilities: UtilitiesConfig {} + property SidebarConfig sidebar: SidebarConfig {} property ServiceConfig services: ServiceConfig {} property UserPaths paths: UserPaths {} } diff --git a/config/SidebarConfig.qml b/config/SidebarConfig.qml new file mode 100644 index 0000000..a871562 --- /dev/null +++ b/config/SidebarConfig.qml @@ -0,0 +1,11 @@ +import Quickshell.Io + +JsonObject { + property bool enabled: true + property int dragThreshold: 80 + property Sizes sizes: Sizes {} + + component Sizes: JsonObject { + property int width: 430 + } +} diff --git a/modules/drawers/Backgrounds.qml b/modules/drawers/Backgrounds.qml index e1773a1..7fa2ca1 100644 --- a/modules/drawers/Backgrounds.qml +++ b/modules/drawers/Backgrounds.qml @@ -7,6 +7,7 @@ import qs.modules.launcher as Launcher import qs.modules.dashboard as Dashboard import qs.modules.bar.popouts as BarPopouts import qs.modules.utilities as Utilities +import qs.modules.sidebar as Sidebar import QtQuick import QtQuick.Shapes @@ -24,12 +25,13 @@ Shape { Osd.Background { wrapper: root.panels.osd - startX: root.width - root.panels.session.width + startX: root.width - root.panels.session.width - root.panels.sidebar.width startY: (root.height - wrapper.height) / 2 - rounding } Notifications.Background { wrapper: root.panels.notifications + sidebar: sidebar startX: root.width startY: 0 @@ -38,7 +40,7 @@ Shape { Session.Background { wrapper: root.panels.session - startX: root.width + startX: root.width - root.panels.sidebar.width startY: (root.height - wrapper.height) / 2 - rounding } @@ -66,8 +68,19 @@ Shape { Utilities.Background { wrapper: root.panels.utilities + sidebar: sidebar startX: root.width startY: root.height } + + Sidebar.Background { + id: sidebar + + wrapper: root.panels.sidebar + panels: root.panels + + startX: root.width + startY: root.panels.notifications.height + } } diff --git a/modules/drawers/Drawers.qml b/modules/drawers/Drawers.qml index 5e5e992..3503697 100644 --- a/modules/drawers/Drawers.qml +++ b/modules/drawers/Drawers.qml @@ -72,11 +72,12 @@ Variants { } HyprlandFocusGrab { - active: (visibilities.launcher && Config.launcher.enabled) || (visibilities.session && Config.session.enabled) + active: (visibilities.launcher && Config.launcher.enabled) || (visibilities.session && Config.session.enabled) || (visibilities.sidebar && Config.sidebar.enabled) windows: [win] onCleared: { visibilities.launcher = false; visibilities.session = false; + visibilities.sidebar = false; } } @@ -119,6 +120,7 @@ Variants { property bool launcher property bool dashboard property bool utilities + property bool sidebar Component.onCompleted: Visibilities.load(scope.modelData, this) } diff --git a/modules/drawers/Interactions.qml b/modules/drawers/Interactions.qml index 99aa267..84714b5 100644 --- a/modules/drawers/Interactions.qml +++ b/modules/drawers/Interactions.qml @@ -82,6 +82,8 @@ CustomMouseArea { const x = event.x; const y = event.y; + const dragX = x - dragStart.x; + const dragY = y - dragStart.y; // Show bar in non-exclusive mode on hover if (!visibilities.bar && Config.bar.showOnHover && x < bar.implicitWidth) @@ -89,33 +91,68 @@ CustomMouseArea { // Show/hide bar on drag if (pressed && dragStart.x < bar.implicitWidth) { - const dragX = x - dragStart.x; if (dragX > Config.bar.dragThreshold) visibilities.bar = true; else if (dragX < -Config.bar.dragThreshold) visibilities.bar = false; } - // Show osd on hover - const showOsd = inRightPanel(panels.osd, x, y); + if (panels.sidebar.width === 0) { + // Show osd on hover + const showOsd = inRightPanel(panels.osd, x, y); - // Always update visibility based on hover if not in shortcut mode - if (!osdShortcutActive) { - visibilities.osd = showOsd; - root.panels.osd.hovered = showOsd; - } else if (showOsd) { - // If hovering over OSD area while in shortcut mode, transition to hover control - osdShortcutActive = false; - root.panels.osd.hovered = true; - } + // Always update visibility based on hover if not in shortcut mode + if (!osdShortcutActive) { + visibilities.osd = showOsd; + root.panels.osd.hovered = showOsd; + } else if (showOsd) { + // If hovering over OSD area while in shortcut mode, transition to hover control + osdShortcutActive = false; + root.panels.osd.hovered = true; + } + + const showSidebar = pressed && dragStart.x > bar.implicitWidth + panels.sidebar.x; + + // Show/hide session on drag + if (pressed && inRightPanel(panels.session, dragStart.x, dragStart.y) && withinPanelHeight(panels.session, x, y)) { + if (dragX < -Config.session.dragThreshold) + visibilities.session = true; + else if (dragX > Config.session.dragThreshold) + visibilities.session = false; + + // Show sidebar on drag if in session area and session is nearly fully visible + if (showSidebar && panels.session.width >= panels.session.nonAnimWidth && dragX < -Config.sidebar.dragThreshold) + visibilities.sidebar = true; + } else if (showSidebar && dragX < -Config.sidebar.dragThreshold) { + // Show sidebar on drag if not in session area + visibilities.sidebar = true; + } + } else { + const outOfSidebar = x < width - panels.sidebar.width; + // Show osd on hover + const showOsd = outOfSidebar && inRightPanel(panels.osd, x, y); + + // Always update visibility based on hover if not in shortcut mode + if (!osdShortcutActive) { + visibilities.osd = showOsd; + root.panels.osd.hovered = showOsd; + } else if (showOsd) { + // If hovering over OSD area while in shortcut mode, transition to hover control + osdShortcutActive = false; + root.panels.osd.hovered = true; + } + + // Show/hide session on drag + if (pressed && outOfSidebar && inRightPanel(panels.session, dragStart.x, dragStart.y) && withinPanelHeight(panels.session, x, y)) { + if (dragX < -Config.session.dragThreshold) + visibilities.session = true; + else if (dragX > Config.session.dragThreshold) + visibilities.session = false; + } - // Show/hide session on drag - if (pressed && inRightPanel(panels.session, dragStart.x, dragStart.y) && withinPanelHeight(panels.session, x, y)) { - const dragX = x - dragStart.x; - if (dragX < -Config.session.dragThreshold) - visibilities.session = true; - else if (dragX > Config.session.dragThreshold) - visibilities.session = false; + // Hide sidebar on drag + if (pressed && inRightPanel(panels.sidebar, dragStart.x, 0) && dragX > Config.sidebar.dragThreshold) + visibilities.sidebar = false; } // Show launcher on hover, or show/hide on drag if hover is disabled @@ -123,7 +160,6 @@ CustomMouseArea { if (!visibilities.launcher && inBottomPanel(panels.launcher, x, y)) visibilities.launcher = true; } else if (pressed && inBottomPanel(panels.launcher, dragStart.x, dragStart.y) && withinPanelWidth(panels.launcher, x, y)) { - const dragY = y - dragStart.y; if (dragY < -Config.launcher.dragThreshold) visibilities.launcher = true; else if (dragY > Config.launcher.dragThreshold) @@ -143,7 +179,6 @@ CustomMouseArea { // Show/hide dashboard on drag (for touchscreen devices) if (pressed && inTopPanel(panels.dashboard, dragStart.x, dragStart.y) && withinPanelWidth(panels.dashboard, x, y)) { - const dragY = y - dragStart.y; if (dragY > Config.dashboard.dragThreshold) visibilities.dashboard = true; else if (dragY < -Config.dashboard.dragThreshold) diff --git a/modules/drawers/Panels.qml b/modules/drawers/Panels.qml index ed21f80..7896ddb 100644 --- a/modules/drawers/Panels.qml +++ b/modules/drawers/Panels.qml @@ -6,6 +6,7 @@ import qs.modules.launcher as Launcher import qs.modules.dashboard as Dashboard import qs.modules.bar.popouts as BarPopouts import qs.modules.utilities as Utilities +import qs.modules.sidebar as Sidebar import Quickshell import QtQuick @@ -23,6 +24,7 @@ Item { readonly property Dashboard.Wrapper dashboard: dashboard readonly property BarPopouts.Wrapper popouts: popouts readonly property Utilities.Wrapper utilities: utilities + readonly property Sidebar.Wrapper sidebar: sidebar anchors.fill: parent anchors.margins: Config.border.thickness @@ -31,20 +33,20 @@ Item { Osd.Wrapper { id: osd - clip: root.visibilities.session + clip: session.width > 0 || sidebar.width > 0 screen: root.screen visibilities: root.visibilities anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right - anchors.rightMargin: session.width + anchors.rightMargin: session.width + sidebar.width } Notifications.Wrapper { id: notifications visibilities: root.visibilities - panel: root + panels: root anchors.top: parent.top anchors.right: parent.right @@ -53,10 +55,13 @@ Item { Session.Wrapper { id: session + clip: sidebar.width > 0 visibilities: root.visibilities + panels: root anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right + anchors.rightMargin: sidebar.width } Launcher.Wrapper { @@ -101,8 +106,20 @@ Item { id: utilities visibilities: root.visibilities + sidebar: sidebar anchors.bottom: parent.bottom anchors.right: parent.right } + + Sidebar.Wrapper { + id: sidebar + + visibilities: root.visibilities + panels: root + + anchors.top: notifications.bottom + anchors.bottom: utilities.top + anchors.right: parent.right + } } diff --git a/modules/notifications/Background.qml b/modules/notifications/Background.qml index 47ec54a..a44cb19 100644 --- a/modules/notifications/Background.qml +++ b/modules/notifications/Background.qml @@ -1,7 +1,6 @@ import qs.components import qs.services import qs.config -import Quickshell import QtQuick import QtQuick.Shapes @@ -9,10 +8,10 @@ ShapePath { id: root required property Wrapper wrapper + required property var sidebar readonly property real rounding: Config.border.rounding readonly property bool flatten: wrapper.height < rounding * 2 readonly property real roundingY: flatten ? wrapper.height / 2 : rounding - property real fullHeightRounding: wrapper.height >= QsWindow.window?.height - Config.border.thickness * 2 ? -rounding : rounding strokeWidth: -1 fillColor: Colours.palette.m3surface @@ -32,14 +31,14 @@ ShapePath { relativeY: root.wrapper.height - root.roundingY * 2 } PathArc { - relativeX: root.fullHeightRounding + relativeX: root.sidebar.notifsRoundingX relativeY: root.roundingY - radiusX: Math.abs(root.fullHeightRounding) + radiusX: root.sidebar.notifsRoundingX radiusY: Math.min(root.rounding, root.wrapper.height) - direction: root.fullHeightRounding < 0 ? PathArc.Clockwise : PathArc.Counterclockwise + direction: PathArc.Counterclockwise } PathLine { - relativeX: root.wrapper.height > 0 ? root.wrapper.width - root.rounding - root.fullHeightRounding : root.wrapper.width + relativeX: root.wrapper.height > 0 ? root.wrapper.width - root.rounding - root.sidebar.notifsRoundingX : root.wrapper.width relativeY: 0 } PathArc { @@ -52,8 +51,4 @@ ShapePath { Behavior on fillColor { CAnim {} } - - Behavior on fullHeightRounding { - Anim {} - } } diff --git a/modules/notifications/Content.qml b/modules/notifications/Content.qml index 2ece7f4..828c3a8 100644 --- a/modules/notifications/Content.qml +++ b/modules/notifications/Content.qml @@ -10,7 +10,7 @@ Item { id: root required property PersistentProperties visibilities - required property Item panel + required property Item panels readonly property int padding: Appearance.padding.large anchors.top: parent.top @@ -27,15 +27,15 @@ Item { for (let i = 0; i < count; i++) height += list.itemAtIndex(i)?.nonAnimHeight ?? 0; - if (visibilities && panel) { + if (visibilities && panels) { if (visibilities.osd) { - const h = panel.osd.y - Config.border.rounding * 2 - padding * 2; + const h = panels.osd.y - Config.border.rounding * 2 - padding * 2; if (height > h) height = h; } if (visibilities.session) { - const h = panel.session.y - Config.border.rounding * 2 - padding * 2; + const h = panels.session.y - Config.border.rounding * 2 - padding * 2; if (height > h) height = h; } diff --git a/modules/notifications/Wrapper.qml b/modules/notifications/Wrapper.qml index 47655ac..61acc56 100644 --- a/modules/notifications/Wrapper.qml +++ b/modules/notifications/Wrapper.qml @@ -1,21 +1,39 @@ +import qs.components import qs.config -import Quickshell import QtQuick Item { id: root - required property PersistentProperties visibilities - required property Item panel + required property var visibilities + required property Item panels visible: height > 0 + implicitWidth: Math.max(panels.sidebar.width, content.implicitWidth) implicitHeight: content.implicitHeight - implicitWidth: content.implicitWidth + + states: State { + name: "hidden" + when: root.visibilities.sidebar && Config.sidebar.enabled + + PropertyChanges { + root.implicitHeight: 0 + } + } + + transitions: Transition { + Anim { + target: root + property: "implicitHeight" + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } + } Content { id: content visibilities: root.visibilities - panel: root.panel + panels: root.panels } } diff --git a/modules/session/Wrapper.qml b/modules/session/Wrapper.qml index 967d8c3..14b03a8 100644 --- a/modules/session/Wrapper.qml +++ b/modules/session/Wrapper.qml @@ -9,6 +9,8 @@ Item { id: root required property PersistentProperties visibilities + required property var panels + readonly property real nonAnimWidth: content.implicitWidth visible: width > 0 implicitWidth: 0 @@ -19,7 +21,7 @@ Item { when: root.visibilities.session && Config.session.enabled PropertyChanges { - root.implicitWidth: content.implicitWidth + root.implicitWidth: root.nonAnimWidth } } @@ -41,7 +43,7 @@ Item { Anim { target: root property: "implicitWidth" - easing.bezierCurve: root.visibilities.osd ? Appearance.anim.curves.expressiveDefaultSpatial : Appearance.anim.curves.emphasized + easing.bezierCurve: root.panels.osd.width > 0 ? Appearance.anim.curves.expressiveDefaultSpatial : Appearance.anim.curves.emphasized } } ] diff --git a/modules/sidebar/Background.qml b/modules/sidebar/Background.qml new file mode 100644 index 0000000..beefdf5 --- /dev/null +++ b/modules/sidebar/Background.qml @@ -0,0 +1,52 @@ +import qs.components +import qs.services +import qs.config +import QtQuick +import QtQuick.Shapes + +ShapePath { + id: root + + required property Wrapper wrapper + required property var panels + + readonly property real rounding: Config.border.rounding + + readonly property real notifsWidthDiff: panels.notifications.width - wrapper.width + readonly property real notifsRoundingX: panels.notifications.height > 0 && notifsWidthDiff < rounding * 2 ? notifsWidthDiff / 2 : rounding + + readonly property real utilsWidthDiff: panels.utilities.width - wrapper.width + readonly property real utilsRoundingX: utilsWidthDiff < rounding * 2 ? utilsWidthDiff / 2 : rounding + + strokeWidth: -1 + fillColor: Colours.palette.m3surface + + PathLine { + relativeX: -root.wrapper.width - root.notifsRoundingX + relativeY: 0 + } + PathArc { + relativeX: root.notifsRoundingX + relativeY: root.rounding + radiusX: root.notifsRoundingX + radiusY: root.rounding + } + PathLine { + relativeX: 0 + relativeY: root.wrapper.height - root.rounding * 2 + } + PathArc { + relativeX: -root.utilsRoundingX + relativeY: root.rounding + radiusX: root.utilsRoundingX + radiusY: root.rounding + } + PathLine { + relativeX: root.wrapper.width + root.utilsRoundingX + relativeY: 0 + } + + Behavior on fillColor { + CAnim {} + } +} diff --git a/modules/sidebar/Content.qml b/modules/sidebar/Content.qml new file mode 100644 index 0000000..13829bd --- /dev/null +++ b/modules/sidebar/Content.qml @@ -0,0 +1,19 @@ +import qs.config +import QtQuick +import QtQuick.Layouts + +Item { + id: root + + required property var visibilities + + implicitWidth: layout.implicitWidth + implicitHeight: layout.implicitHeight + + ColumnLayout { + id: layout + + anchors.fill: parent + spacing: Appearance.spacing.normal + } +} diff --git a/modules/sidebar/Wrapper.qml b/modules/sidebar/Wrapper.qml new file mode 100644 index 0000000..ddbb39c --- /dev/null +++ b/modules/sidebar/Wrapper.qml @@ -0,0 +1,66 @@ +pragma ComponentBehavior: Bound + +import qs.components +import qs.config +import QtQuick + +Item { + id: root + + required property var visibilities + required property var panels + + visible: width > 0 + implicitWidth: 0 + implicitHeight: 0 + + states: State { + name: "visible" + when: root.visibilities.sidebar && Config.sidebar.enabled + + PropertyChanges { + root.implicitWidth: Config.sidebar.sizes.width + } + } + + transitions: [ + Transition { + from: "" + to: "visible" + + Anim { + target: root + property: "implicitWidth" + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } + }, + Transition { + from: "visible" + to: "" + + Anim { + target: root + property: "implicitWidth" + easing.bezierCurve: root.panels.osd.width > 0 || root.panels.session.width > 0 ? Appearance.anim.curves.expressiveDefaultSpatial : Appearance.anim.curves.emphasized + } + } + ] + + Loader { + id: content + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: Appearance.padding.large + + visible: false + active: true + Component.onCompleted: active = Qt.binding(() => (root.visibilities.sidebar && Config.sidebar.enabled) || root.visible) + + sourceComponent: Content { + visibilities: root.visibilities + } + } +} diff --git a/modules/utilities/Background.qml b/modules/utilities/Background.qml index 5dc2c13..fbce896 100644 --- a/modules/utilities/Background.qml +++ b/modules/utilities/Background.qml @@ -8,6 +8,7 @@ ShapePath { id: root required property Wrapper wrapper + required property var sidebar readonly property real rounding: Config.border.rounding readonly property bool flatten: wrapper.height < rounding * 2 readonly property real roundingY: flatten ? wrapper.height / 2 : rounding @@ -31,13 +32,13 @@ ShapePath { relativeY: -(root.wrapper.height - root.roundingY * 2) } PathArc { - relativeX: root.rounding + relativeX: root.sidebar.utilsRoundingX relativeY: -root.roundingY - radiusX: root.rounding + radiusX: root.sidebar.utilsRoundingX radiusY: Math.min(root.rounding, root.wrapper.height) } PathLine { - relativeX: root.wrapper.height > 0 ? root.wrapper.width - root.rounding * 2 : root.wrapper.width + relativeX: root.wrapper.height > 0 ? root.wrapper.width - root.rounding - root.sidebar.utilsRoundingX : root.wrapper.width relativeY: 0 } PathArc { diff --git a/modules/utilities/Wrapper.qml b/modules/utilities/Wrapper.qml index 3f84319..a3ba53d 100644 --- a/modules/utilities/Wrapper.qml +++ b/modules/utilities/Wrapper.qml @@ -9,6 +9,8 @@ Item { id: root required property var visibilities + required property Item sidebar + readonly property PersistentProperties props: PersistentProperties { property bool recordingListExpanded: false property string recordingConfirmDelete @@ -16,10 +18,11 @@ Item { reloadableId: "utilities" } + readonly property bool shouldBeActive: visibilities.sidebar || (visibilities.utilities && Config.utilities.enabled) visible: height > 0 implicitHeight: 0 - implicitWidth: Config.utilities.sizes.width + implicitWidth: Math.max(sidebar.width, Config.utilities.sizes.width) onStateChanged: { if (state === "visible" && timer.running) { @@ -30,7 +33,7 @@ Item { states: State { name: "visible" - when: root.visibilities.utilities + when: root.shouldBeActive PropertyChanges { root.implicitHeight: content.implicitHeight + Appearance.padding.large * 2 @@ -67,7 +70,7 @@ Item { running: true interval: Appearance.anim.durations.extraLarge onTriggered: { - content.active = Qt.binding(() => (root.visibilities.utilities && Config.utilities.enabled) || root.visible); + content.active = Qt.binding(() => root.shouldBeActive || root.visible); content.visible = true; } } @@ -77,13 +80,13 @@ Item { anchors.top: parent.top anchors.left: parent.left - anchors.right: parent.right anchors.margins: Appearance.padding.large visible: false active: true sourceComponent: Content { + implicitWidth: Config.utilities.sizes.width - Appearance.padding.large * 2 props: root.props visibilities: root.visibilities } -- cgit v1.2.3-freya