diff --git a/settings/quickshell.nix b/settings/quickshell.nix index 238b64b..151ab21 100644 --- a/settings/quickshell.nix +++ b/settings/quickshell.nix @@ -68,7 +68,6 @@ in vec4 cutout; // cx, cy, hw, hh — rounded inner screen cutout vec4 panel; // cx, cy, hw, hh — dropdown panel (hw <= 0: none) vec4 toast; // cx, cy, hw, hh — toast (hw <= 0: none) - vec4 session; // cx, cy, hw, hh — session menu (hw <= 0: none) vec4 launcher; // cx, cy, hw, hh — bottom launcher (hw <= 0: none) vec4 fillColor; // straight (non-premultiplied) rgba vec4 borderColor; @@ -100,8 +99,6 @@ in d = smin(d, sdRoundedBox(p, panel.xy, panel.zw, panelR), meltK); if (toast.z > 0.5) d = smin(d, sdRoundedBox(p, toast.xy, toast.zw, panelR), meltK); - if (session.z > 0.5) - d = smin(d, sdRoundedBox(p, session.xy, session.zw, panelR), meltK); if (launcher.z > 0.5) d = smin(d, sdRoundedBox(p, launcher.xy, launcher.zw, panelR), meltK); @@ -183,9 +180,9 @@ in // here, not in ~15 places. Wide enough for the stacked HH/mm // clock, which is what sets the floor. readonly property int barWidth: 46 - readonly property int radius: 16 // cards, panels, dropdowns - readonly property int radiusSmall: 12 // workspace dots, pill buttons - readonly property int radiusTiny: 8 // hover rows, action chips + readonly property int radius: 32 // cards, panels, dropdowns + readonly property int radiusSmall: 24 // workspace dots, pill buttons + readonly property int radiusTiny: 16 // hover rows, action chips readonly property int cardPad: 8 // card inner padding (inset = 2×) readonly property int panelGap: 8 // gap between panel border and cards (all dropdowns/menus/toast) @@ -632,7 +629,7 @@ in // (caelestia's): the grab redirects focus to this window and // OnDemand lets the layer surface accept it. Exclusive fights // the grab — it self-clears and instantly closes the panel. - WlrLayershell.keyboardFocus: (sessionMenu.open || launcherPanel.open || bar.activeDropdown !== null) && !bar.screenshotPinned ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None + WlrLayershell.keyboardFocus: (launcherPanel.open || bar.activeDropdown !== null) && !bar.screenshotPinned ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None // Vertical bar on the left edge. The surface still covers the // whole screen (the frame band and every panel are drawn into @@ -661,12 +658,6 @@ in width: toastItem.visible ? toastItem.width : 0 height: toastItem.visible ? toastItem.height : 0 } - Region { - x: sessionMenu.visible ? sessionMenu.x : 0 - y: sessionMenu.visible ? sessionMenu.y : 0 - width: sessionMenu.visible ? sessionMenu.width : 0 - height: sessionMenu.visible ? sessionMenu.height : 0 - } Region { x: launcherPanel.visible ? launcherPanel.x : 0 y: launcherPanel.visible ? launcherPanel.y : 0 @@ -691,7 +682,10 @@ in } function toggleSession() { - sessionMenu.toggle(); + bar.toggleDropdown(sessionMenu, function() { + let pos = powerWidget.mapToItem(bar.contentItem, 0, powerWidget.height / 2); + sessionMenu.dropdownY = pos.y; + }); } function toggleLauncher() { @@ -1000,12 +994,6 @@ in ? Qt.vector4d(surfLeftX + _toastRect.width / 2, (meltTop + toastBot) / 2, 4 + _toastRect.width / 2, (toastBot - meltTop) / 2) : Qt.vector4d(0, 0, 0, 0) - // Session menu sits in the bottom-left corner and melts - // down into the bottom frame band. - property vector4d session: sessionMenu.visible - ? Qt.vector4d(surfLeftX + sessionMenu.width / 2, (sessionMenu.y + meltBot) / 2, - 4 + sessionMenu.width / 2, (meltBot - sessionMenu.y) / 2) - : Qt.vector4d(0, 0, 0, 0) // The launcher is the one surface that stays put: it still // grows from the bottom frame band at screen centre. readonly property real launchBot: bar.height - Theme.frameWidth + 4 @@ -1023,142 +1011,6 @@ in fragmentShader: "file://${chromeShader}" } - // ── Session menu: icon-only power controls morphing out of - // the bar at the bottom-left (Super+L, or the power button). - // Keyboard: arrows/Tab move the selection, Enter activates, - // Esc closes. - Item { - id: sessionMenu - property bool open: false - property int selIdx: 0 - // Both axes animate so the panel expands from a small - // point on the column (like the top dropdowns' stub seed) - // instead of rolling out at full height. - property real openW: open ? sessionCard.width + 2 * Theme.panelGap : 0 - property real openH: open ? sessionCard.height + 2 * Theme.panelGap : 36 - Behavior on openW { - NumberAnimation { duration: Theme.animMorph; easing.type: Easing.OutExpo } - } - Behavior on openH { - NumberAnimation { duration: Theme.animMorph; easing.type: Easing.OutExpo } - } - - readonly property var actions: [ - { icon: "lock", danger: false, act: "lock" }, - { icon: "logout", danger: false, act: "logout" }, - { icon: "restart_alt", danger: true, act: "reboot" }, - { icon: "power_settings_new", danger: true, act: "poweroff" } - ] - - function activate(act) { - open = false; - if (act === "lock") bar.shellRoot.lock(); - else if (act === "logout") Hyprland.dispatch("hl.dsp.exit()"); - else if (act === "reboot") Quickshell.execDetached([Commands.systemctl, "reboot"]); - else Quickshell.execDetached([Commands.systemctl, "poweroff"]); - } - - function toggle() { - open = !open; - if (open) { - selIdx = 0; - forceActiveFocus(); - } - } - - // Unfolds rightward out of the bar at the bottom-left, - // level with the power button that opens it. Bottom edge - // sits on the frame's inner edge so the shader can melt - // the two together (meltBot) instead of leaving a seam. - x: Theme.barWidth - y: Math.round(bar.height - Theme.frameWidth - height) - width: openW - height: openH - visible: openW > 0.5 - - focus: open - Keys.onEscapePressed: open = false - Keys.onUpPressed: selIdx = (selIdx + actions.length - 1) % actions.length - Keys.onDownPressed: selIdx = (selIdx + 1) % actions.length - Keys.onTabPressed: selIdx = (selIdx + 1) % actions.length - Keys.onReturnPressed: activate(actions[selIdx].act) - Keys.onEnterPressed: activate(actions[selIdx].act) - - // Content pinned to the column edge, revealed by the grow - Item { - anchors.fill: parent - clip: true - opacity: sessionMenu.open ? 1 : 0 - Behavior on opacity { - NumberAnimation { duration: Theme.animContent; easing.type: Easing.OutCubic } - } - - // Card backing, matching the other dropdowns - Rectangle { - id: sessionCard - anchors.verticalCenter: parent.verticalCenter - anchors.left: parent.left - anchors.leftMargin: Theme.panelGap - width: 48 - height: sessionCol.height + 8 - radius: Theme.radius - color: Theme.cardBg - - // Sliding selection pill — same tech as the power - // profile selector; glides between the buttons. - Rectangle { - width: 40 - height: 40 - radius: Theme.radius - color: Theme.base02 - border.width: 1 - border.color: Theme.base03 - x: sessionCol.x - y: sessionCol.y + sessionMenu.selIdx * 44 - Behavior on y { - NumberAnimation { duration: 250; easing.type: Easing.OutExpo } - } - } - - Column { - id: sessionCol - anchors.centerIn: parent - spacing: 4 - - Repeater { - model: sessionMenu.actions - - Item { - id: sessBtn - required property var modelData - required property int index - width: 40 - height: 40 - - SIcon { - anchors.centerIn: parent - text: sessBtn.modelData.icon - // All four buttons share the logout - // setup: base05, FILL on selection. - color: Theme.base05 - font.pixelSize: 20 - font.weight: 600 - } - - MouseArea { - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onEntered: sessionMenu.selIdx = sessBtn.index - onClicked: sessionMenu.activate(sessBtn.modelData.act) - } - } - } - } - } - } - } - // ── Launcher: rises out of the bottom frame edge (Super+R). // Lives in the SDF field, so it melts into the frame and its // height morphs live as results filter. Results sit above the @@ -1375,11 +1227,10 @@ in // closes whatever is open. Suspended while screenshotPinned so // slurp can grab input without dismissing the menu. HyprlandFocusGrab { - active: (sessionMenu.open || launcherPanel.open || bar.activeDropdown !== null) && !bar.screenshotPinned + active: (launcherPanel.open || bar.activeDropdown !== null) && !bar.screenshotPinned windows: [bar] onCleared: { if (bar.screenshotPinned) return; - sessionMenu.open = false; launcherPanel.open = false; bar.closeAllDropdowns(); } @@ -2044,6 +1895,11 @@ in hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: bar.toggleSession() + onEntered: { + if (bar.activeDropdown) { + if (bar.activeDropdown !== sessionMenu) bar.toggleSession(); + } + } } } } @@ -2099,7 +1955,11 @@ in x: Theme.barWidth y: Math.round(Math.max(yMin, Math.min(yMax, dropdownY - height / 2))) width: fullWidth + 4 - height: fullHeight + 16 + // +24, not +16: the shader insets the panel 8px top and + // bottom, so +16 left the panel edge flush with the content + // (0 padding) while the growth axis overhung by 4. The + // extra 8 gives 4px above and below, matching the right. + height: fullHeight + 24 visible: false onVisibleChanged: { @@ -2138,7 +1998,9 @@ in Item { id: dropdownContent - y: 8 - _dropdownRect.y + // 12 = panel inset (8) + the 4px breathing room, + // keeping the content centred in the taller panel. + y: 12 - _dropdownRect.y width: dropdown.fullWidth height: dropdown.fullHeight } @@ -2298,6 +2160,104 @@ in } } + // ── Session menu: icon-only power controls morphing out of + // the bar at the bottom-left (Super+L, or the power button). + // Keyboard: arrows/Tab move the selection, Enter activates, + // Esc closes. + BarDropdown { + id: sessionMenu + fullWidth: 48 + 2 * Theme.panelGap + fullHeight: sessionCard.height + 2 * Theme.panelGap + property int selIdx: 0 + + readonly property var actions: [ + { icon: "lock", danger: false, act: "lock" }, + { icon: "logout", danger: false, act: "logout" }, + { icon: "restart_alt", danger: true, act: "reboot" }, + { icon: "power_settings_new", danger: true, act: "poweroff" } + ] + + function activate(act) { + animateClose(); + if (act === "lock") bar.shellRoot.lock(); + else if (act === "logout") Hyprland.dispatch("hl.dsp.exit()"); + else if (act === "reboot") Quickshell.execDetached([Commands.systemctl, "reboot"]); + else Quickshell.execDetached([Commands.systemctl, "poweroff"]); + } + + focus: open + onOpenChanged: if (open) { selIdx = 0; forceActiveFocus(); } + Keys.onEscapePressed: bar.closeAllDropdowns() + Keys.onUpPressed: selIdx = (selIdx + actions.length - 1) % actions.length + Keys.onDownPressed: selIdx = (selIdx + 1) % actions.length + Keys.onTabPressed: selIdx = (selIdx + 1) % actions.length + Keys.onReturnPressed: activate(actions[selIdx].act) + Keys.onEnterPressed: activate(actions[selIdx].act) + + // Card backing, matching the other dropdowns. The grow / + // clip reveal is BarDropdown's job now. + Rectangle { + id: sessionCard + anchors.centerIn: parent + width: 48 + height: sessionCol.height + 8 + radius: Theme.radius + color: Theme.cardBg + + // Sliding selection pill — same tech as the power + // profile selector; glides between the buttons. + Rectangle { + width: 40 + height: 40 + radius: Theme.radius + color: Theme.base02 + border.width: 1 + border.color: Theme.base03 + x: sessionCol.x + y: sessionCol.y + sessionMenu.selIdx * 44 + Behavior on y { + NumberAnimation { duration: 250; easing.type: Easing.OutExpo } + } + } + + Column { + id: sessionCol + anchors.centerIn: parent + spacing: 4 + + Repeater { + model: sessionMenu.actions + + Item { + id: sessBtn + required property var modelData + required property int index + width: 40 + height: 40 + + SIcon { + anchors.centerIn: parent + text: sessBtn.modelData.icon + // All four buttons share the logout + // setup: base05, FILL on selection. + color: Theme.base05 + font.pixelSize: 20 + font.weight: 600 + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onEntered: sessionMenu.selIdx = sessBtn.index + onClicked: sessionMenu.activate(sessBtn.modelData.act) + } + } + } + } + } + } + // Volume dropdown BarDropdown { id: volDropdown