diff --git a/quickshell/agility/Core/Services/AppState.qml b/quickshell/agility/Core/Services/AppState.qml index 4ee4c325..c2b70f60 100644 --- a/quickshell/agility/Core/Services/AppState.qml +++ b/quickshell/agility/Core/Services/AppState.qml @@ -1,5 +1,7 @@ import QtQuick import Quickshell +import Quickshell.Hyprland +import Quickshell.Io import qs.Core import qs.Core.Services pragma Singleton @@ -25,6 +27,9 @@ Item { property bool hasActiveNotification: false property real activeNotificationWidth: 0 property real activeNotificationHeight: 0 + readonly property bool isNiri: Quickshell.env("NIRI_SOCKET") !== undefined || Quickshell.env("XDG_CURRENT_DESKTOP") === "niri" + property bool hasActiveWindow: false + readonly property bool hasWindowOnScreen: isNiri ? hasActiveWindow : Boolean(Hyprland.focusedWorkspace && Hyprland.focusedWorkspace.toplevelCount > 0) readonly property var focusWidgets: ["launcher", "clipboard", "wallpaper", "screenshot", "powerMenu"] readonly property var standaloneWindows: ["minflair_settings", "minflair_keybinds", "packagemanager"] @@ -179,4 +184,81 @@ Item { onOpenWidget: (widgetId) => { openWidgetInternal(widgetId); } + + // ─── Niri IPC Engine for Window Presence Tracking ─── + Process { + id: niriQueryProc + + command: ["niri", "msg", "-j", "workspaces"] + + stdout: SplitParser { + onRead: (data) => { + if (!data) + return; + try { + let list = JSON.parse(data.trim()); + if (!Array.isArray(list)) + return; + + let hasWin = false; + for (let i = 0; i < list.length; i++) { + let ws = list[i]; + if (ws.is_active || ws.is_focused) { + if (ws.active_window_id !== null && ws.active_window_id !== undefined) + hasWin = true; + } + } + appState.hasActiveWindow = hasWin; + } catch (e) { + } + } + } + } + + Process { + id: niriEventStreamProc + + command: ["niri", "msg", "event-stream"] + + stdout: SplitParser { + onRead: (line) => { + if (!line) + return; + if (line.includes("Workspace") || line.includes("Window")) { + niriDebounceTimer.restart(); + } + } + } + } + + Timer { + id: niriDebounceTimer + + interval: 50 + repeat: false + onTriggered: { + if (appState.isNiri && !niriQueryProc.running) + niriQueryProc.running = true; + } + } + + Timer { + id: niriPollTimer + + interval: 2000 + repeat: true + running: appState.isNiri + triggeredOnStart: true + onTriggered: { + if (!niriQueryProc.running) + niriQueryProc.running = true; + } + } + + Component.onCompleted: { + if (appState.isNiri) { + niriQueryProc.running = true; + niriEventStreamProc.running = true; + } + } } diff --git a/quickshell/agility/Core/Services/SettingsService.qml b/quickshell/agility/Core/Services/SettingsService.qml index 45234e9f..2f63b68f 100644 --- a/quickshell/agility/Core/Services/SettingsService.qml +++ b/quickshell/agility/Core/Services/SettingsService.qml @@ -30,6 +30,10 @@ Item { property bool barConvexMode: barStyle === "convex" property bool barPillMode: barStyle === "pill" property string islandRightMode: "auto" + property int pillBarWidthPercent: 60 + property string pillPopupStyle: "center" + property string pillWidgetLayout: "distributed" + property bool barAutoHide: false readonly property int barWidgetHeight: 28 property bool edgeLightingEnabled: true property string edgeLightingColorMode: "album_art" @@ -180,6 +184,26 @@ Item { if (settingsLoaded) saveSettings(); + } + onPillBarWidthPercentChanged: { + if (settingsLoaded) + saveSettings(); + + } + onPillPopupStyleChanged: { + if (settingsLoaded) + saveSettings(); + + } + onPillWidgetLayoutChanged: { + if (settingsLoaded) + saveSettings(); + + } + onBarAutoHideChanged: { + if (settingsLoaded) + saveSettings(); + } onEdgeLightingEnabledChanged: { if (settingsLoaded) saveSettings(); } onEdgeLightingColorModeChanged: { if (settingsLoaded) saveSettings(); } @@ -237,6 +261,10 @@ Item { "cursorSize": settingsService.cursorSize, "barStyle": settingsService.barStyle, "islandRightMode": settingsService.islandRightMode, + "pillBarWidthPercent": settingsService.pillBarWidthPercent, + "pillPopupStyle": settingsService.pillPopupStyle, + "pillWidgetLayout": settingsService.pillWidgetLayout, + "barAutoHide": settingsService.barAutoHide, "actionShortcuts": settingsService.actionShortcuts, "customShortcuts": settingsService.customShortcuts, "edgeLightingEnabled": settingsService.edgeLightingEnabled, @@ -404,6 +432,25 @@ cp ~/.cache/quickshell/settings_prefs.json /var/tmp/sddm-theme/settings_prefs.js if (prefs.islandRightMode !== undefined) settingsService.islandRightMode = prefs.islandRightMode; + if (prefs.pillBarWidthPercent !== undefined) { + let pbw = parseInt(prefs.pillBarWidthPercent); + if (!isNaN(pbw) && pbw >= 30 && pbw <= 100) + settingsService.pillBarWidthPercent = pbw; + } + + if (prefs.pillPopupStyle !== undefined) { + if (prefs.pillPopupStyle === "sided" || prefs.pillPopupStyle === "top" || prefs.pillPopupStyle === "center") + settingsService.pillPopupStyle = prefs.pillPopupStyle; + } + + if (prefs.pillWidgetLayout !== undefined) { + if (prefs.pillWidgetLayout === "distributed" || prefs.pillWidgetLayout === "centered") + settingsService.pillWidgetLayout = prefs.pillWidgetLayout; + } + + if (prefs.barAutoHide !== undefined) + settingsService.barAutoHide = Boolean(prefs.barAutoHide); + if (prefs.actionShortcuts !== undefined) settingsService.actionShortcuts = prefs.actionShortcuts; diff --git a/quickshell/agility/Core/Windows/OverlayStyles/IslandOverlayStyle.qml b/quickshell/agility/Core/Windows/OverlayStyles/IslandOverlayStyle.qml index f1a55cd3..2f6d589c 100644 --- a/quickshell/agility/Core/Windows/OverlayStyles/IslandOverlayStyle.qml +++ b/quickshell/agility/Core/Windows/OverlayStyles/IslandOverlayStyle.qml @@ -31,21 +31,27 @@ Item { AppState.isIslandOpen = true; let nw = (AppState.hasActiveNotification && AppState.activeNotificationWidth > 0) ? AppState.activeNotificationWidth : 0; let nh = (AppState.hasActiveNotification && AppState.activeNotificationHeight > 0) ? AppState.activeNotificationHeight : 0; - mainContainer.fromWidth = nw > 0 ? nw : ((AppState.barIslandWidth > 0) ? AppState.barIslandWidth : ((AppState.islandWidth > 0) ? AppState.islandWidth : mainContainer.fromWidth)); - mainContainer.fromHeight = nh > 0 ? nh : ((AppState.barIslandHeight > 0) ? AppState.barIslandHeight : ((AppState.islandHeight > 0) ? AppState.islandHeight : mainContainer.fromHeight)); + let pillW = (SettingsService.barPillMode && root.width > 0) ? Math.round(root.width * (SettingsService.pillBarWidthPercent / 100)) : 0; + let baseW = pillW > 0 ? pillW : ((AppState.barIslandWidth > 0) ? AppState.barIslandWidth : ((AppState.islandWidth > 0) ? AppState.islandWidth : mainContainer.fromWidth)); + let baseH = SettingsService.barPillMode ? 38 : ((AppState.barIslandHeight > 0) ? AppState.barIslandHeight : ((AppState.islandHeight > 0) ? AppState.islandHeight : mainContainer.fromHeight)); + mainContainer.fromWidth = nw > 0 ? nw : baseW; + mainContainer.fromHeight = nh > 0 ? nh : baseH; mainContainer.fromRadius = nh > 0 ? Constants.sizeSm : mainContainer.fromHeight / 2; } else { let nw = (AppState.hasActiveNotification && AppState.activeNotificationWidth > 0) ? AppState.activeNotificationWidth : 0; let nh = (AppState.hasActiveNotification && AppState.activeNotificationHeight > 0) ? AppState.activeNotificationHeight : 0; - mainContainer.fromWidth = nw > 0 ? nw : ((AppState.barIslandWidth > 0) ? AppState.barIslandWidth : ((AppState.islandWidth > 0) ? AppState.islandWidth : mainContainer.fromWidth)); - mainContainer.fromHeight = nh > 0 ? nh : ((AppState.barIslandHeight > 0) ? AppState.barIslandHeight : ((AppState.islandHeight > 0) ? AppState.islandHeight : mainContainer.fromHeight)); + let pillW = (SettingsService.barPillMode && root.width > 0) ? Math.round(root.width * (SettingsService.pillBarWidthPercent / 100)) : 0; + let baseW = pillW > 0 ? pillW : ((AppState.barIslandWidth > 0) ? AppState.barIslandWidth : ((AppState.islandWidth > 0) ? AppState.islandWidth : mainContainer.fromWidth)); + let baseH = SettingsService.barPillMode ? 38 : ((AppState.barIslandHeight > 0) ? AppState.barIslandHeight : ((AppState.islandHeight > 0) ? AppState.islandHeight : mainContainer.fromHeight)); + mainContainer.fromWidth = nw > 0 ? nw : baseW; + mainContainer.fromHeight = nh > 0 ? nh : baseH; mainContainer.fromRadius = nh > 0 ? Constants.sizeSm : mainContainer.fromHeight / 2; } } Connections { function onFullyClosed() { - if (SettingsService.barIslandMode && !widget.positionAtBottom) { + if ((SettingsService.barIslandMode || (SettingsService.barPillMode && SettingsService.pillPopupStyle === "top")) && !widget.positionAtBottom) { isHandover = true; AppState.isIslandOpen = false; if (AppState.activeWidget !== "") @@ -73,8 +79,8 @@ Item { readonly property real notifW: (AppState.hasActiveNotification && AppState.activeNotificationWidth > 0) ? AppState.activeNotificationWidth : 0 readonly property real notifH: (AppState.hasActiveNotification && AppState.activeNotificationHeight > 0) ? AppState.activeNotificationHeight : 0 - property real fromWidth: notifW > 0 ? notifW : ((AppState.barIslandWidth > 0) ? AppState.barIslandWidth : ((AppState.islandWidth > 0) ? AppState.islandWidth : 180)) - property real fromHeight: notifH > 0 ? notifH : ((AppState.barIslandHeight > 0) ? AppState.barIslandHeight : ((AppState.islandHeight > 0) ? AppState.islandHeight : Constants.size3Xl)) + property real fromWidth: notifW > 0 ? notifW : (SettingsService.barPillMode && root.width > 0 ? Math.round(root.width * (SettingsService.pillBarWidthPercent / 100)) : ((AppState.barIslandWidth > 0) ? AppState.barIslandWidth : ((AppState.islandWidth > 0) ? AppState.islandWidth : 180))) + property real fromHeight: notifH > 0 ? notifH : (SettingsService.barPillMode ? 38 : ((AppState.barIslandHeight > 0) ? AppState.barIslandHeight : ((AppState.islandHeight > 0) ? AppState.islandHeight : Constants.size3Xl))) property real fromRadius: notifH > 0 ? Constants.sizeXl : fromHeight / 2 readonly property real openY: 8 readonly property real currentWidth: Math.round(fromWidth + (preferredWidth - fromWidth) * bounceProgress) @@ -91,7 +97,7 @@ Item { IslandStyle.IslandBackground { anchors.fill: parent radius: mainContainer.currentRadius - enableShadow: SettingsService.barIslandMode && !isHandover + enableShadow: (SettingsService.barIslandMode || SettingsService.barPillMode) && !isHandover } Item { diff --git a/quickshell/agility/Core/Windows/OverlayWindow.qml b/quickshell/agility/Core/Windows/OverlayWindow.qml index c0f528ac..f131c278 100644 --- a/quickshell/agility/Core/Windows/OverlayWindow.qml +++ b/quickshell/agility/Core/Windows/OverlayWindow.qml @@ -41,7 +41,7 @@ PanelWindow { property bool windowFocusable: true // Determine the active style mode cleanly readonly property string activeStyle: { - if (SettingsService.barIslandMode && !positionAtBottom) + if ((SettingsService.barIslandMode || (SettingsService.barPillMode && SettingsService.pillPopupStyle === "top")) && !positionAtBottom) return "island"; return "floating"; diff --git a/quickshell/agility/Core/Windows/SidebarWindow.qml b/quickshell/agility/Core/Windows/SidebarWindow.qml index 991e53c5..7cc0c74f 100644 --- a/quickshell/agility/Core/Windows/SidebarWindow.qml +++ b/quickshell/agility/Core/Windows/SidebarWindow.qml @@ -17,7 +17,7 @@ Item { property int flareHeight: Constants.size2Xl + 4 property int flareWidth: Constants.size2Xl + 4 property bool alignTop: false - property int topOffset: (SettingsService.barStyle === "convex" ? 48 : 24) + property int topOffset: (SettingsService.barStyle === "convex" || (SettingsService.barPillMode && SettingsService.pillPopupStyle === "sided") ? 48 : 24) readonly property int verticalPadding: flareHeight + contentPadding default property alias content: innerLayout.data property int _screenHeight: 1080 diff --git a/quickshell/agility/Core/Windows/WidgetLoader.qml b/quickshell/agility/Core/Windows/WidgetLoader.qml index 1304227c..bf689088 100644 --- a/quickshell/agility/Core/Windows/WidgetLoader.qml +++ b/quickshell/agility/Core/Windows/WidgetLoader.qml @@ -14,7 +14,8 @@ Item { property bool _isInternalActive: false readonly property bool isBarOverlayHandled: AppState.isBarOverlayWidget(widgetId) readonly property bool isIslandHandled: !DisplayProfileService.gameModeActive && SettingsService.barIslandMode && isBarOverlayHandled - readonly property bool isConvexHandled: !DisplayProfileService.gameModeActive && SettingsService.barConvexMode && isBarOverlayHandled + readonly property bool isSidedPillHandled: SettingsService.barPillMode && SettingsService.pillPopupStyle === "sided" && (widgetId !== "music") && isBarOverlayHandled + readonly property bool isConvexHandled: !DisplayProfileService.gameModeActive && ((SettingsService.barConvexMode && isBarOverlayHandled) || isSidedPillHandled) readonly property bool isGamingHandled: DisplayProfileService.gameModeActive && isBarOverlayHandled readonly property bool shouldLoadWindow: enabled && !isIslandHandled && !isConvexHandled && !isGamingHandled readonly property bool _isActive: shouldLoadWindow && (exclusive ? AppState.isWidgetOpen(widgetId) : _isInternalActive) diff --git a/quickshell/agility/Modules/Bar/BarOverlayWindow.qml b/quickshell/agility/Modules/Bar/BarOverlayWindow.qml index 8f1cc3a1..8a0be888 100644 --- a/quickshell/agility/Modules/Bar/BarOverlayWindow.qml +++ b/quickshell/agility/Modules/Bar/BarOverlayWindow.qml @@ -8,6 +8,7 @@ import qs.Core import qs.Core.Services import qs.Modules.Bar import qs.Modules.Bar.Components +import qs.Modules.Bar.Styles.Convex PanelWindow { id: root @@ -25,9 +26,91 @@ PanelWindow { readonly property bool usesFloatingPopups: styleData.usesFloatingPopups readonly property bool isPill: styleData.isPill readonly property QtObject activeHost: SettingsService.barIslandMode ? barStrip : null - readonly property bool needsFocus: (activeHost && activeHost.isOpen && activeHost.needsFocus) + readonly property bool isSidedPill: SettingsService.barPillMode && SettingsService.pillPopupStyle === "sided" + readonly property bool sidedNeedsFocus: isSidedPill && AppState.isFocusWidgetOpen && (pillPanelsLoader.item && pillPanelsLoader.item.hasAnyDrawerOpen) + readonly property bool needsFocus: (activeHost && activeHost.isOpen && activeHost.needsFocus) || sidedNeedsFocus + + // Auto-hide configuration & states + readonly property real barHiddenY: -(barStrip.height + barStrip.y + 16) + readonly property bool autoHideEnabled: SettingsService.barAutoHide && (root.isPill || SettingsService.barIslandMode) + readonly property bool hasAnyOpenWidget: AppState.isFocusWidgetOpen + || AppState.activeWidget !== "" + || AppState.activeWidgetsList.length > 0 + || (root.activeHost && (root.activeHost.isOpen || root.activeHost.isOccupied)) + || (pillPanelsLoader.item && (pillPanelsLoader.item.hasAnyPanelOpen || pillPanelsLoader.item.isConvexCenterOpen)) + || AppState.activeTrayItem !== null + || AppState.hasActiveNotification + property bool isHoverRevealed: (barHoverHandler.hovered || topEdgeHoverHandler.hovered) + readonly property bool shouldBarHide: autoHideEnabled && AppState.hasWindowOnScreen && !hasAnyOpenWidget && !isHoverRevealed + readonly property bool isBarHidden: barTranslate.y <= (barHiddenY + 4) + + onShouldBarHideChanged: { + if (shouldBarHide) { + hideDebounceTimer.restart(); + } else { + hideDebounceTimer.stop(); + barSlideUpAnim.stop(); + barSlideDownAnim.restart(); + } + } + + Timer { + id: hideDebounceTimer + + interval: 350 + repeat: false + onTriggered: { + if (root.shouldBarHide) { + barSlideDownAnim.stop(); + barSlideUpAnim.restart(); + } + } + } + + ParallelAnimation { + id: barSlideUpAnim + + NumberAnimation { + target: barTranslate + property: "y" + to: root.barHiddenY + duration: Constants.animNormal + easing.type: Easing.OutCubic + } + + NumberAnimation { + target: barStrip + property: "opacity" + to: 0 + duration: Constants.animNormal + easing.type: Easing.OutCubic + } + } + + ParallelAnimation { + id: barSlideDownAnim + + NumberAnimation { + target: barTranslate + property: "y" + to: 0 + duration: Constants.animNormal + easing.type: Easing.OutCubic + } + + NumberAnimation { + target: barStrip + property: "opacity" + to: 1 + duration: Constants.animNormal + easing.type: Easing.OutCubic + } + } function triggerStyleSwitch() { + barSlideUpAnim.stop(); + barSlideDownAnim.stop(); + hideDebounceTimer.stop(); if (!root.isShellReady || !SettingsService.settingsLoaded) { root.activeBarStyle = SettingsService.barStyle; return ; @@ -83,6 +166,11 @@ PanelWindow { WlrLayershell.layer: WlrLayer.Top WlrLayershell.exclusionMode: ExclusionMode.Ignore WlrLayershell.keyboardFocus: needsFocus ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None + onNeedsFocusChanged: { + if (sidedNeedsFocus && pillPanelsLoader.item && pillPanelsLoader.item.bottomDrawer) { + pillPanelsLoader.item.bottomDrawer.activateCurrentItem(); + } + } color: "transparent" Component.onCompleted: { activeBarStyle = SettingsService.barStyle; @@ -104,6 +192,9 @@ PanelWindow { barExitOnlyAnim.stop(); barStyleSwitchAnim.stop(); barEntranceAnim.stop(); + barSlideUpAnim.stop(); + barSlideDownAnim.stop(); + hideDebounceTimer.stop(); barStrip.opacity = 1; barTranslate.y = 0; barStrip.scale = 1; @@ -127,6 +218,8 @@ PanelWindow { if (activeHost && activeHost.isOpen) activeHost.close(); + if (root.isSidedPill && AppState.activeWidget !== "") + AppState.activeWidget = ""; } } } @@ -315,6 +408,22 @@ PanelWindow { anchors.fill: parent layer.enabled: false + Loader { + id: pillPanelsLoader + + anchors.fill: parent + active: root.isSidedPill + visible: active + z: 20 + sourceComponent: Component { + ConvexPanels { + barHeight: root.currentBarHeight + bezelSize: 0 + notificationService: root.notificationService + } + } + } + MouseArea { id: dismissArea @@ -353,6 +462,41 @@ PanelWindow { ] } + // Top edge reveal trigger (active only when bar is auto-hidden) + Item { + id: topEdgeTrigger + + anchors.top: parent.top + x: root.isPill ? (barStrip.x + barStrip.centerX) : barStrip.x + width: root.isPill ? barStrip.centerWidth : barStrip.width + height: 4 + visible: root.autoHideEnabled && root.isBarHidden + z: 99 + + HoverHandler { + id: topEdgeHoverHandler + enabled: parent.visible + } + } + + // Bar hover tracker (matches visible pill/island capsule bounds) + Item { + id: barHoverTracker + + x: root.isPill ? (barStrip.x + barStrip.centerX) : barStrip.x + y: Math.max(0, barStrip.y + barTranslate.y) + width: root.isPill ? barStrip.centerWidth : barStrip.width + height: barStrip.height + visible: !root.isBarHidden + z: 40 + + HoverHandler { + id: barHoverHandler + enabled: root.autoHideEnabled && parent.visible + margin: 4 + } + } + layer.effect: MultiEffect { shadowEnabled: true shadowColor: Theme.shadow @@ -365,12 +509,20 @@ PanelWindow { } mask: Region { - // Center (Island) + // Center (Island or Pill) + Region { + x: root.isPill ? (barStrip.x + barStrip.centerX) : barStrip.x + y: Math.max(0, barStrip.y + barTranslate.y) + width: root.isPill ? barStrip.centerWidth : barStrip.width + height: Math.max(0, barStrip.height + Math.min(0, barStrip.y + barTranslate.y)) + } + + // Top edge trigger when bar is hidden under auto-hide Region { - x: root.isPill ? (barStrip.x + barStrip.centerX - 16) : (barStrip.x - 16) - y: Math.max(0, barStrip.y - 24) - width: root.isPill ? (barStrip.centerWidth + 32) : (barStrip.width + 32) - height: barStrip.height + 40 + x: (root.autoHideEnabled && root.isBarHidden) ? (root.isPill ? (barStrip.x + barStrip.centerX) : barStrip.x) : 0 + y: 0 + width: (root.autoHideEnabled && root.isBarHidden) ? (root.isPill ? barStrip.centerWidth : barStrip.width) : 0 + height: (root.autoHideEnabled && root.isBarHidden) ? 4 : 0 } // Expandable Host Overlay @@ -383,6 +535,16 @@ PanelWindow { height: isActive ? (root.activeHost.isOpen ? root.height : root.activeHost.blockHeight) : 0 } + // Sided Pill Panels Overlay (Full Screen for Clicks/Focus when Sided Pill has open panel) + Region { + property bool isActive: root.isSidedPill && pillPanelsLoader.item && (pillPanelsLoader.item.hasAnyPanelOpen || pillPanelsLoader.item.isConvexCenterOpen) + + x: 0 + y: 0 + width: isActive ? root.width : 0 + height: isActive ? root.height : 0 + } + } } diff --git a/quickshell/agility/Modules/Bar/Styles/Convex/ConvexRightHost.qml b/quickshell/agility/Modules/Bar/Styles/Convex/ConvexRightHost.qml index 00309b36..8a9504d0 100644 --- a/quickshell/agility/Modules/Bar/Styles/Convex/ConvexRightHost.qml +++ b/quickshell/agility/Modules/Bar/Styles/Convex/ConvexRightHost.qml @@ -204,7 +204,7 @@ Item { positionAtLeft: false backgroundColor: Theme.bg alignTop: root.displayedWidgetId.startsWith("systemTray_") - topOffset: SettingsService.barConvexMode ? 80 : Constants.size4Xl + topOffset: (SettingsService.barConvexMode || (SettingsService.barPillMode && SettingsService.pillPopupStyle === "sided")) ? 80 : Constants.size4Xl preferredWidth: 0 preferredHeight: 0 // Exact original dimensions per widget diff --git a/quickshell/agility/Modules/Bar/Styles/Pill/PillBar.qml b/quickshell/agility/Modules/Bar/Styles/Pill/PillBar.qml index 6621098f..ba453225 100644 --- a/quickshell/agility/Modules/Bar/Styles/Pill/PillBar.qml +++ b/quickshell/agility/Modules/Bar/Styles/Pill/PillBar.qml @@ -44,16 +44,27 @@ Item { Rectangle { id: pillContainer + readonly property real minPillWidth: Math.max(leftGroup.implicitWidth + centerGroup.implicitWidth + rightGroup.implicitWidth + (Constants.sizeLg * 4), 480) + readonly property real targetPillWidth: Math.round(root.width * (SettingsService.pillBarWidthPercent / 100)) + readonly property bool isFullWidth: SettingsService.pillBarWidthPercent >= 100 + anchors.horizontalCenter: parent.horizontalCenter anchors.verticalCenter: parent.verticalCenter height: root.baseBarHeight - width: Math.max(contentRow.implicitWidth + (Constants.sizeLg * 2), 480) + width: Math.max(minPillWidth, Math.min(root.width - (isFullWidth ? 0 : Constants.sizeMd * 2), targetPillWidth)) implicitWidth: width - radius: height / 2 + radius: isFullWidth ? Constants.sizeSm : height / 2 color: Theme.bg border.color: Theme.borderSecondary border.width: 1 + Behavior on width { + NumberAnimation { + duration: Constants.animFast + easing.type: Easing.OutQuad + } + } + layer.enabled: true layer.effect: DropShadow { transparentBorder: true @@ -67,78 +78,115 @@ Item { RowLayout { id: contentRow - anchors.centerIn: parent + readonly property bool isDistributed: SettingsService.pillWidgetLayout === "distributed" + + anchors.fill: isDistributed ? parent : undefined + anchors.centerIn: isDistributed ? undefined : parent + anchors.leftMargin: isDistributed ? Constants.sizeLg : 0 + anchors.rightMargin: isDistributed ? Constants.sizeLg : 0 spacing: Constants.sizeSm // Left Navigation (Material 3 Dashboard, Compositor Workspaces) - MinflairButton { - id: minflairBtn + RowLayout { + id: leftGroup + spacing: Constants.sizeSm Layout.alignment: Qt.AlignVCenter - } - Workspaces { - id: workspacesWidget - Layout.alignment: Qt.AlignVCenter - visible: true + MinflairButton { + id: minflairBtn + Layout.alignment: Qt.AlignVCenter + } + + Workspaces { + id: workspacesWidget + Layout.alignment: Qt.AlignVCenter + visible: true + } + + // Subtle Visual Divider + Rectangle { + width: 1 + height: 16 + color: Theme.borderSecondary + Layout.alignment: Qt.AlignVCenter + opacity: 0.6 + } } - // Subtle Visual Divider - Rectangle { - width: 1 - height: 16 - color: Theme.borderSecondary - Layout.alignment: Qt.AlignVCenter - opacity: 0.6 + // Expanding spacer between Left and Center + Item { + Layout.fillWidth: contentRow.isDistributed + Layout.minimumWidth: 0 + visible: contentRow.isDistributed } // Center (Clock & Live Playing Media) - ClockButton { - id: clockBtn + RowLayout { + id: centerGroup + spacing: Constants.sizeSm Layout.alignment: Qt.AlignVCenter - } - MediaButton { - id: mediaBtn - Layout.alignment: Qt.AlignVCenter - } + ClockButton { + id: clockBtn + Layout.alignment: Qt.AlignVCenter + } - // Subtle Visual Divider - Rectangle { - width: 1 - height: 16 - color: Theme.borderSecondary - Layout.alignment: Qt.AlignVCenter - opacity: 0.6 + MediaButton { + id: mediaBtn + Layout.alignment: Qt.AlignVCenter + } } - // Right Quick Controls (Recording, Tray, QuickSettings, Notifications, Power) - RecordingIndicator { - id: recIndicator - Layout.alignment: Qt.AlignVCenter - visible: RecorderService.running - } - - SystemTrayGroup { - id: systemTray - Layout.alignment: Qt.AlignVCenter + // Expanding spacer between Center and Right + Item { + Layout.fillWidth: contentRow.isDistributed + Layout.minimumWidth: 0 + visible: contentRow.isDistributed } - ControlCenterButton { - id: ccBtn - Layout.alignment: Qt.AlignVCenter - controlCenterWidget: root.controlCenterWidget - } - - NotificationsButton { - id: notifBtn + // Right Quick Controls (Recording, Tray, QuickSettings, Notifications, Power) + RowLayout { + id: rightGroup + spacing: Constants.sizeSm Layout.alignment: Qt.AlignVCenter - notificationsCenterWidget: root.notificationsCenterWidget - notificationService: root.notificationService - } - PowerButton { - id: powerBtn - Layout.alignment: Qt.AlignVCenter + // Subtle Visual Divider + Rectangle { + width: 1 + height: 16 + color: Theme.borderSecondary + Layout.alignment: Qt.AlignVCenter + opacity: 0.6 + } + + RecordingIndicator { + id: recIndicator + Layout.alignment: Qt.AlignVCenter + visible: RecorderService.running + } + + SystemTrayGroup { + id: systemTray + Layout.alignment: Qt.AlignVCenter + } + + ControlCenterButton { + id: ccBtn + Layout.alignment: Qt.AlignVCenter + controlCenterWidget: root.controlCenterWidget + } + + NotificationsButton { + id: notifBtn + Layout.alignment: Qt.AlignVCenter + notificationsCenterWidget: root.notificationsCenterWidget + notificationService: root.notificationService + } + + PowerButton { + id: powerBtn + Layout.alignment: Qt.AlignVCenter + } } } } diff --git a/quickshell/agility/Modules/Settings/BarSettings/BarSettings.qml b/quickshell/agility/Modules/Settings/BarSettings/BarSettings.qml index 6451ebc9..f1dcba3a 100644 --- a/quickshell/agility/Modules/Settings/BarSettings/BarSettings.qml +++ b/quickshell/agility/Modules/Settings/BarSettings/BarSettings.qml @@ -33,6 +33,20 @@ AppContainer { } } + Divider { + visible: SettingsService.barIslandMode || SettingsService.barPillMode + } + + SettingToggle { + visible: SettingsService.barIslandMode || SettingsService.barPillMode + label: "Auto-hide Bar" + description: "Smoothly slide the bar out of view when a window is open, reveal when hovered or screen is empty" + checked: SettingsService.barAutoHide + onCheckedChanged: { + SettingsService.barAutoHide = checked; + } + } + } // ISLAND MODE WIDGETS @@ -62,6 +76,68 @@ AppContainer { } + // PILL MODE SETTINGS + AppGroup { + title: "Pill Bar Customization" + icon: "tune" + visible: SettingsService.barPillMode + + SettingSegmented { + label: "Popup Style" + description: "Choose how options and menus open (Sided, Top, or Center)" + model: [{ + "text": "Sided", + "value": "sided" + }, { + "text": "Top", + "value": "top" + }, { + "text": "Center", + "value": "center" + }] + currentValue: SettingsService.pillPopupStyle + onActivated: (value) => { + SettingsService.pillPopupStyle = value; + } + } + + Divider { + } + + SettingSegmented { + label: "Widget Layout" + description: "Choose how widgets are arranged across the bar" + model: [{ + "text": "Distributed", + "value": "distributed" + }, { + "text": "Centered", + "value": "centered" + }] + currentValue: SettingsService.pillWidgetLayout + onActivated: (value) => { + SettingsService.pillWidgetLayout = value; + } + } + + Divider { + } + + ThemedSlider { + label: "Bar Width" + from: 30 + to: 100 + stepSize: 1 + value: SettingsService.pillBarWidthPercent + suffix: "%" + decimals: 0 + onMoved: (val) => { + SettingsService.pillBarWidthPercent = Math.round(val); + } + } + + } + AppGroup { title: "Clock Format" icon: "clock"