feat: add sidebar

WIP
This commit is contained in:
2 * r + 2 * t 2025-09-18 15:19:15 +10:00
parent 2f42377479
commit 85d575f6c1
16 changed files with 296 additions and 55 deletions

View file

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

View file

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

11
config/SidebarConfig.qml Normal file
View file

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

View file

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

View file

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

View file

@ -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,13 +91,13 @@ 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;
}
if (panels.sidebar.width === 0) {
// Show osd on hover
const showOsd = inRightPanel(panels.osd, x, y);
@ -109,13 +111,48 @@ CustomMouseArea {
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)) {
const dragX = x - dragStart.x;
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;
}
// 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)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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